Automated Threat Modeling

Brief Summary: Gravity Forms CVE-2026-90437 Unauthenticated Stored XSS and What the 3.1.2 Patch Changed

A brief summary of CVE-2026-90437, an unauthenticated stored cross-site scripting flaw in Gravity Forms through 3.1.1, where entity-encoded data attributes survive wp_kses_post() and reach insertAdjacentHTML() in admin JavaScript. Includes a review of the server-side changes in the 3.1.2 patch.

CVE Analysis

9 min read

Gaurav Sarraf
Gaurav Sarraf

2026-11-03

Brief Summary: Gravity Forms CVE-2026-90437 Unauthenticated Stored XSS and What the 3.1.2 Patch Changed
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 stored cross-site scripting flaw in Gravity Forms lets anonymous form submitters plant markup that later executes in the browser of a user who views the injected content, via the plugin's own administrative JavaScript. The payload travels as entity-encoded text inside a data-dialog-confirm attribute, and that text passes the save-time sanitizer most WordPress developers would expect to stop it.

Gravity Forms is a commercial form management plugin for WordPress developed by Rocketgenius Inc. The vendor states that it powers more than 5 million websites worldwide, which is a vendor footprint claim rather than an independently measured count of vulnerable installations. Forms are frequently the only part of a WordPress site that accepts input from anonymous visitors, so the way a forms plugin stores and renders submissions sits directly on the trust boundary between the public internet and site administrators.

Technical information

Root cause

The CVE record classifies the issue as CWE-79, improper neutralization of input during web page generation, caused by insufficient input sanitization and output escaping. No WordPress account is required to submit the payload.

Three field types are involved, and all three return true from allow_html(), meaning they accept raw HTML submissions:

  • Post Excerpt
  • Post Custom Field
  • Paragraph Text, but only when its rich text editor is enabled. Plain Paragraph Text configurations are not named in the advisory.

The failure spans two separate trust boundaries:

  1. Storage. Values from these fields pass through wp_kses_post() when saved. WordPress documents this function as allowing the permitted post HTML tags and attributes, and the post allowlist permits data-* attributes. An entity-encoded payload placed inside data-dialog-confirm therefore survives sanitization as harmless-looking attribute text.
  2. Rendering. When the browser reads the attribute, it decodes the entities. The plugin's admin JavaScript then passes that decoded string to insertAdjacentHTML(). MDN documents insertAdjacentHTML() as an injection sink that parses its input into the DOM and does not sanitize unsafe elements or event handler attributes.

On the PHP side, every rendering path in 3.1.1 trusted the value that had been saved to the database. The pattern repeated across output paths looked like this:

// The value contains HTML but the value was sanitized before saving. $return = $value;

That comment records the design assumption that broke. A sanitizer run once at save time was treated as a guarantee about every later use of the value. The stored payload never needed a <script> tag or an on* event handler. It only becomes dangerous when admin JavaScript reads the attribute and feeds it to an HTML-parsing sink.

Attack flow

Based on the advisory, the documented sequence is:

  1. An unauthenticated attacker submits an entity-encoded payload through a Post Excerpt, Post Custom Field, or rich-text Paragraph Text field on a public form.
  2. The payload is stored inside a data-dialog-confirm attribute and passes wp_kses_post() at save time.
  3. A user later accesses a page where the stored value is rendered. The browser decodes the attribute content.
  4. The plugin's administrative JavaScript passes the decoded value to insertAdjacentHTML(), and the content becomes executable markup in that user's browser session.

The available records do not identify a specific administrative screen, a complete exploit request, or a confirmed server compromise resulting from this CVE.

Scoring and exploitation status

The CNA vector is CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:L/A:N for a score of 7.2. NVD displays this CNA assessment but has not supplied an independent NIST score. Patchstack scores the issue 7.1 and states that a privileged user must perform an action for successful exploitation. The CVE narrative itself describes execution when a user accesses the injected page, so we think it is reasonable to model this as unauthenticated submission followed by execution when a victim views the content, while keeping both published scores in view. The reviewed sources do not explain the disagreement.

As of October 30, 2026, Wiz reported no public proof of concept, no evidence of exploitation in the wild, and no actor attribution. TheHackerWire reports no CISA KEV listing. Patchstack expects the issue to become exploited and says it may appear in mass exploitation campaigns, which is a prediction rather than an observation. Wordfence credits the researcher daroo with the discovery.

Patch information

Gravity Forms 3.1.2 fixes CVE-2026-90437. Wordfence marks the issue as patched and lists the remediation as "Update to version 3.1.2, or a newer patched version." The vendor's 3.1.2 release notes do not name the CVE. They only say "Added security enhancements."

To see what actually changed, we compared 3.1.2 against 3.1.1 using commit 87f9f72 ("Updates to 3.1.2") in the pronamic/gravityforms GitHub repository, which is a third-party mirror of vendor releases.

Fix 1: a narrower allowlist with no data-* attributes

3.1.2 adds a new method, GF_Field::get_entry_allowed_html(), to the base field class. It starts from the standard post allowlist and removes the data-* wildcard from every tag:

public function get_entry_allowed_html( $allowable_tags = true ) { $allowed = wp_kses_allowed_html( 'post' ); foreach ( $allowed as $tag => $attributes ) { // wp_kses normally allows data-* attributes, so we explicitly remove them preventing stored entry values from activating admin behaviors such as data-dialog-confirm. unset( $attributes['data-*'] ); $allowed[ $tag ] = $attributes; } if ( $allowable_tags === true ) { return $allowed; }

The method also handles a form's custom allowed tags setting. It reads tag names from that setting (either a string or an array) and rejects any name that does not start with a lowercase letter followed only by lowercase letters, digits, or hyphens. Tags that also appear in the post allowlist keep their normal attributes, still without data-*. Tags outside that list are allowed with no attributes at all.

The main design decision is to strip the entire data-* family. The payload lived in a data attribute that only becomes dangerous when admin JavaScript consumes it, so removing the attribute family means no stored entry value can trigger admin behaviors such as data-dialog-confirm.

Fix 2: sanitizing again at output

The patch removes the "sanitized before saving" comment everywhere and runs wp_kses() with the new allowlist each time a value is rendered. In the base GF_Field class this covers get_value_merge_tag(), get_value_entry_list() and get_value_entry_detail().

Before (3.1.1):

// The value contains HTML but the value was sanitized before saving. $return = $value;

After (3.1.2):

$return = wp_kses( $value, $this->get_entry_allowed_html( $allowable_tags ) );

The same change appears in field classes that override these methods:

  • Post Excerpt: the merge tag output receives the identical replacement shown above.
  • Paragraph Text / rich text: values are now filtered before wpautop() in both entry detail and merge tag output.

Before (3.1.1):

$return = wpautop( $value );

After (3.1.2):

$return = wpautop( wp_kses( $value, $this->get_entry_allowed_html() ) );
  • Single Line Text: the same fix in merge tag, entry list and entry detail output.
  • Repeater: subfield values were already filtered with wp_kses_allowed_html( 'post' ), which still allowed data-*. They now use $this->get_entry_allowed_html().

Why output-time filtering matters

If the patch had only tightened sanitization at save time, malicious entries already sitting in the database would still fire after the upgrade. Because 3.1.2 filters values when they are rendered, a stored data-dialog-confirm payload displayed through these paths loses the attribute once the site is upgraded. Input is sanitized when saved, and output is still treated as untrusted.

Other changes in the same release

The 3.1.2 commit also rebuilt the minified admin JavaScript bundles, including a renamed form editor chunk. Because the files are minified, the diff does not show whether the insertAdjacentHTML() call itself changed. The fix we can confirm is the server-side data-* removal and output filtering.

The release also contains unrelated hardening, which suggests the "security enhancements" bundle addressed several issues:

  • An HMAC (hash_hmac( 'sha256', ..., wp_salt( 'auth' ) )) to verify temporary upload files.
  • Size limits on chunked uploads.
  • esc_html() for Phone field entry list output.
  • esc_url() for Website field entry list output.

After upgrading

Wiz recommends auditing existing entries for injected JavaScript and monitoring suspicious submissions. Vendor guidance also supports keeping an active, authenticated license key in plugin settings, updating Gravity Forms before its add-ons, and making sure Rocketgenius security notification emails reach administrators. Least-privilege capabilities, forms restricted to logged-in users where appropriate, and Content Security Policy are documented hardening measures, but none of them is a verified substitute for installing 3.1.2.

Affected systems and versions

  • Product: Gravity Forms plugin for WordPress (Rocketgenius Inc.)
  • Affected: all versions up to and including 3.1.1
  • Fixed: 3.1.2 or a newer patched version
  • Vulnerable configurations: forms containing a Post Excerpt field, a Post Custom Field, or a Paragraph Text field with the rich text editor enabled

The vendor's 3.1.1 announcement says it added security enhancements, yet Wordfence explicitly includes 3.1.1 in the affected range. The boundary comes from the advisory rather than from release note language.

Wordfence dates its advisory to October 29, 2026, while CVE.org and NVD record publication on October 30, 2026. These are separate publication events for the same vulnerability.

Vendor security history

CVE-2023-28782. Gravity Forms through 2.7.3 was affected by an unauthenticated PHP object injection that Patchstack says could be triggered in a default configuration when a form contained a list field. Version 2.7.4 fixed it, and NVD independently identifies the affected range. Patchstack noted it did not find a significant property-oriented programming chain in the plugin at publication, which limited the demonstrated impact.

July 2025 package tampering. Rocketgenius disclosed that an external agent maliciously modified a limited number of manually downloadable 2.9.11.1 and 2.9.12 packages on July 9 and 10, including certain Composer installations of 2.9.11.1. The vendor says the modified code blocked update attempts and contacted an external server for an additional payload that could attempt to create an administrator account. The vendor released a clean 2.9.13, scanned packages, changed credentials, and audited administrative accounts. SecurityWeek independently reported the affected versions. This was a distribution integrity failure and is unrelated to CVE-2026-90437.

Disclosure process. The vendor documents a responsible disclosure process in which reports are acknowledged, verified and assessed for severity, with fixes delivered in an upcoming release or an immediate security release depending on severity.

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