Automated Threat Modeling

Quick Look: Ninja Forms CVE-2026-109997 Unauthenticated Stored XSS via Metadata Key Normalization

A brief summary of CVE-2026-109997, an unauthenticated stored XSS in the Ninja Forms WordPress plugin through 3.15.5, caused by a reserved metadata key check that runs before WordPress normalizes the key. Covers the root cause, the 3.15.6 patch and practical detection approaches.

CVE Analysis

10 min read

Gaurav Sarraf
Gaurav Sarraf

2026-11-03

Quick Look: Ninja Forms CVE-2026-109997 Unauthenticated Stored XSS via Metadata Key Normalization
Experimental AI-Generated Content

This CVE analysis is an experimental publication that is completely AI-generated. The content may contain errors or inaccuracies and is subject to change as more information becomes available. We are continuously refining our process.

If you have feedback, questions, or notice any errors, please reach out to us.

[email protected]

Introduction

A public contact form is supposed to be the one part of a WordPress site that strangers can safely write to. In Ninja Forms versions up to and including 3.15.5, an anonymous visitor could use that same submission path to store script content that later executes in the browser of whoever opens the submission in the WordPress admin, which in practice means site administrators.

Ninja Forms is a WordPress form builder published by Saturday Drive INC (author Kevin Stover; NVD uses the vendor identifier kstover), used for contact forms, lead generation, calculators, quizzes, surveys, applications, event registration and booking requests. The official WordPress plugin directory lists more than 500,000 active installations, and the vendor advertises over 50 million downloads, although cumulative downloads and active installs are different measures and neither tells us how many sites still run an affected version. Because public forms are reachable by unauthenticated visitors by design, weaknesses in the plugin's submission handling sit on the most exposed surface of a large number of WordPress sites.

Technical Information

Classification and scoring

The weakness is classified as CWE-79 (improper neutralization of input during web page generation). The 7.2 High score shown on NVD was supplied by Wordfence as the CNA. NVD's own assessment is listed as unavailable. The published vector is:

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:L/A:N

That rates the issue as network reachable, low complexity, requiring no privileges, with changed scope and low confidentiality and integrity impact. The vector lists user interaction as none, while the description says the injected script runs whenever a user accesses an injected page. Our reading is that the injection itself needs no interaction, but execution depends on someone viewing the affected submission. We report the score as published rather than rescoring it.

The documented impact is arbitrary script execution in the browser of a user viewing affected content. Neither the CVE description nor the Wordfence advisory claims server-side code execution or a confirmed site takeover for this CVE.

Root cause: a reserved key checked before WordPress normalizes it

Ninja Forms lets a submission carry an extra object inside formData. Some keys in that object are meant to be generated by the server, not the client. One of them is calculations, which the admin Calculations metabox later reads and displays.

In 3.15.5, the submission controller (includes/AJAX/Controllers/Submission.php) tried to stop clients from supplying that key, but only removed it when the key matched calculations exactly:

if( isset( $this->_data['extra']['calculations'] ) ) { unset( $this->_data['extra']['calculations']); unset( $this->_form_data['extra']['calculations']); }

WPSec's analysis explains why this is insufficient. A key containing a backslash does not match the literal string calculations, so it survives the check. Later, when the data is persisted, WordPress's update_post_meta() unslashes the meta key, and the stored key becomes calculations. The attacker-controlled value now sits under the same meta key the server would normally populate itself. This mismatch between the key that was validated and the key that was stored is what Wordfence's advisory title calls Metadata Key Normalization.

Persistence path

The Save action (includes/Actions/Save.php) passes supplied extra data through validateExtraData() and then to update_extra_values(). The submission model's extra value methods (includes/Database/Models/Submission.php) accept metadata keys and values and ultimately write them with update_post_meta(). The validation visible in 3.15.5 consists of array and quantity checks. Those checks constrain the shape of the data but do nothing to make its content safe for HTML output.

Rendering sink: an unescaped array key

The Calculations metabox (includes/Admin/Metaboxes/Calculations.php) retrieves the calculations extra metadata and passes it to includes/Templates/admin-metaboxes-calcs.html.php. In the 3.15.5 template, the calculation value is passed through esc_html(), but the array key $name is echoed directly:

<?php foreach( $data as $name => $contents ):?> <li> <strong><?php echo( $name ); ?></strong> <?php echo( ' = ' . esc_html( $contents[ 'value' ] ) );

Array keys are easy to treat as program controlled, which is probably why this one line was missed while the value, raw and parsed strings were all escaped. Here the key is attacker controlled, so any markup placed in a calculation name is emitted as live HTML when an administrator opens the submission.

The nonce does not act as a barrier

The controller registers wp_ajax_nopriv_nf_ajax_submit, so the submission action is reachable without authentication, and it decodes formData from JSON. When nonce validation fails, the controller generates a fresh nonce and returns it, along with a timestamp, in its response. According to the advisory, this effectively neutralizes the nonce check and makes exploitation achievable in a single unauthenticated request. The public sources establish the handler and the nonce disclosure behavior. They do not include a complete request transcript, and we do not reproduce one here.

Attack flow

Based on the advisory, WPSec's explanation and the 3.15.5 source:

  1. An unauthenticated attacker sends a submission to the nf_ajax_submit AJAX action for a publicly reachable form. If the nonce is missing or invalid, the response hands back a valid new_nonce and nonce_ts pair.
  2. The submission's formData.extra contains a key that resembles calculations but includes a backslash, carrying markup in the calculation name.
  3. The exact-match check in the controller does not recognize the key, so the entry is not removed.
  4. The Save action validates only the shape of the extra data and stores it through update_post_meta(), which unslashes the key to calculations.
  5. Later, an administrator opens the submission under Ninja Forms, Submissions. The Calculations metabox reads the calculations metadata and echoes the attacker-supplied name without escaping.
  6. The script executes in the administrator's authenticated browser session.

Because forms with no configured calculations never receive server-generated calculation data, a Calculations panel on a submission from such a form is a strong sign of injection. We return to this in the detection section.

Exploitation status

The CVE record includes a CISA assessment listing exploitation as none and automatable as no. TheHackerWire reports no confirmed public exploit code, no CISA KEV listing and no known ransomware linkage. No threat actor is named in any reviewed source. Wordfence credits the researcher daroo with the discovery. These statements describe observed activity at the time of reporting. They say nothing about how difficult the flaw is to exploit.

Patch Information

The fix ships in Ninja Forms 3.15.6, released October 15, 2026. Wordfence lists 3.15.6 as the patched version, and the vendor changelog covers it with a general entry about protecting stored submission data against cross-site scripting. The Trac changeset referenced by NVD and Wordfence (r3749135) blocked automated fetching, so the snippets below come from comparing the 3.15.5 and 3.15.6 tags in the WordPress.org SVN repository. The comparison shows two separate fixes: one where the data is displayed, and one where it comes in.

Fix 1: escape the calculation name on output

In 3.15.6 the template escapes the array key as well:

<?php foreach( $data as $name => $contents ):?> <li> <strong><?php echo( esc_html( $name ) ); ?></strong> <?php echo( ' = ' . esc_html( $contents[ 'value' ] ) );

This change is what stops the script from running. Even if bad data still reaches the database, any script tag or event handler markup in a calculation name is now shown as harmless text when an administrator opens the submission.

Fix 2: scrub reserved keys on input, after normalization

Version 3.15.6 adds a dedicated scrub step at the very start of process() in the submission controller:

protected function process() { // Discard client-submitted extra data stored under a reserved, // server-generated key before it enters processing. See issue #8228. $this->scrubReservedExtraKeys();

The reserved keys are defined in a helper that other code can extend through a filter:

protected function getReservedExtraKeys(): array { return apply_filters( 'ninja_forms_reserved_extra_keys', array( 'calculations', 'product_fields', ) ); }

The scrub then compares each key in the form WordPress will store it:

foreach ( array_keys( $this->_form_data[ 'extra' ] ) as $key ) { if ( in_array( wp_unslash( $key ), $reserved, true ) ) { unset( $this->_form_data[ 'extra' ][ $key ] ); } }

The fix turns on the wp_unslash( $key ) call. The old code checked the key before WordPress transformed it; the new code applies the same unslashing first and then checks, so a slashed variant such as calc\ulations can no longer slip through. The strict in_array( ..., true ) comparison avoids type-juggling surprises. Because the scrub runs inside process(), the code comments indicate it covers both fresh submissions and resumed ones after a PayPal or Stripe redirect. The patch also reserves product_fields, another server-generated key, so it cannot be spoofed the same way.

What the patch does not change

The just-in-time nonce logic in submit() is identical in both versions. When nonce validation fails, the plugin still returns a fresh new_nonce and nonce_ts pair. In 3.15.6 the protection comes from rejecting reserved keys on input and escaping calculation names on output, not from a stricter nonce check. With both changes in place, an attacker can no longer plant a fake calculations entry, and any markup that does reach a calculation name is rendered as plain text.

Detection Methods

As of November 3, 2026, no detection content written specifically for CVE-2026-109997 has been published. There are no Sigma, YARA, Snort/Suricata or WAF signatures. Tenable lists no plugins for this CVE, and isMalicious reports no Nuclei template, Exploit DB entry or Metasploit module, with zero indicators linked to the CVE over the prior seven days. Detection therefore rests on three activities: finding vulnerable installs, finding the stored payload, and spotting what happens after it runs.

Find exposed installs

Any Ninja Forms install at version 3.15.5 or earlier is in scope. Version 3.15.6 is fixed. WPScan and Wordfence Intelligence (which offers a free API and webhook feed) both track the issue, so scanners built on either database should flag affected versions. For a single site, notmyfault.fail suggests this WordPress command-line check:

wp plugin list --name=ninja-forms --fields=name,status,version,update,update_version

The same source makes two further points: an inactive status does not prove a site is safe if the plugin files are still deployed, and exposure should be ranked by how many forms anonymous visitors can reach. It also advises against testing by submitting crafted payloads, since version, configuration and log evidence are enough for triage.

Hunt for the stored payload

TrustBrain's analysis of the 3.15.5 to 3.15.6 diff points to the most reliable signal:

  • Submissions from forms that have no calculations configured should never carry calculation data. If a Calculations section appears on one of them, it was very likely injected from outside.
  • Look for characters or strings in calculation names that legitimate labels never use, such as <, script or onerror=.
  • The payload runs when an admin opens the Edit Submission screen, so TrustBrain advises against opening that screen until the plugin is updated. A safer approach is to query stored submission metadata directly for calculations entries on forms without calculations.

Web and application log hunting

No vendor has published a log pattern, but the attack path suggests what to look for in access logs and request body logs from a WAF or reverse proxy:

  • Endpoint: POST requests to the nf_ajax_submit AJAX action, especially from unauthenticated clients.
  • Body content: requests where the extra portion of formData holds a key that looks like calculations but contains backslashes, or values with HTML or script-like content.
  • Context: submissions to forms without calculations that still carry calculation-style extra data.

notmyfault.fail also recommends reviewing recent form submissions, WordPress administrator events and web logs for unexpected changes around public forms. Preserve logs and copies of relevant submissions before making broad changes, and keep in mind that finding nothing suspicious is not proof that exploitation did not occur.

Post-exploitation indicators

Because the script runs in an administrator's browser session, visible damage tends to appear as admin-level actions. TrustBrain specifically warns about new administrator accounts and settings changes. Check the Users list for administrators nobody recognizes, and review audit logs for admin actions shortly after someone viewed a submission's edit screen. Moving the injected submission to trash removes the stored trigger, but only do this after the evidence has been preserved.

Network layer coverage

Generic XSS rules in a WAF may catch obvious script content in form submissions, but every source treats this as best-effort coverage rather than a CVE-specific control or a substitute for patching. Because the bypass relies on a key name that is normalized on save rather than on a particular payload, payload-focused signatures can miss it. The stored data check above is the more dependable control.

Affected Systems and Versions

ReleaseStatus for CVE-2026-109997
Ninja Forms 3.15.5 and all earlier versionsAffected
Ninja Forms 3.15.6 (October 15, 2026)First patched version per Wordfence
Ninja Forms 3.15.7 (November 2, 2026)Newer release with additional hardening

Notes on scope:

  • The vulnerable configuration is any site running an affected version with forms reachable by unauthenticated visitors. The injection path is the public nf_ajax_submit action.
  • Per notmyfault.fail, an installation where the plugin is inactive but its files remain deployed should still be treated as in scope during triage.
  • Version 3.15.7 adds further protections, including merge tag hardening and enforcement of the setting requiring users to be logged in when submitting a form. The changelog does not identify those changes as the fix for this CVE, so a site on 3.15.6 is not affected by CVE-2026-109997 merely because 3.15.7 exists.
  • The separate Ninja Forms File Upload add-on is a different product with its own vulnerability history (see below) and is not the subject of this CVE.

Vendor Security History

This is not the first serious Ninja Forms issue. On June 16, 2022, Wordfence reported a 9.8 Critical code injection vulnerability that allowed unauthenticated invocation of plugin methods and object injection, with arbitrary code execution or file deletion depending on a usable POP chain. Wordfence described evidence suggesting exploitation. The response included fixes backported across multiple release branches, and WordPress appeared to have performed a forced automatic update, although Wordfence cautioned that automatic updates do not always succeed.

In 2026, the separate Ninja Forms File Upload add-on was affected by CVE-2026-0740, rated 9.8 Critical, impacting versions through 3.3.26 and fixed in 3.3.27. Wordfence reported exploitation beginning April 6, 2026, and more than 118,600 blocked exploit attempts at the time of its article. That incident shows attacker interest in the Ninja Forms ecosystem, but it does not establish exploitation of CVE-2026-109997. Inventory the core plugin and its add-ons separately. The 3.15.6 core fix and the 3.3.27 File Upload fix address different products and vulnerabilities.

References

Detect & fix
what others miss

Works with
  • GitHub
  • GitLab
  • Bitbucket
  • Azure DevOps Services
  • Gerrit
  • CVS
  • Jira
  • Linear
  • Slack
  • Security Compass
Explore integrations
Security magnifying glass visualization