
The Gravity Forms plugin for WordPress (tested through version 2.9.28) is vulnerable to unauthenticated reflected cross-site scripting (XSS) via the `form_ids` parameter in the `gform_get_config` AJAX action.
gform_get_config form_ids Parameter| Field | Value |
|---|---|
| Affected Software | Gravity Forms (WordPress Plugin) |
| Vendor | Rocketgenius, Inc. |
| Vulnerability Type | CWE-79: Improper Neutralization of Input During Web Page Generation (Reflected Cross-Site Scripting) |
| CWE Chain | CWE-20 → CWE-116 → CWE-838 → CWE-79 (see CWE Analysis below) |
| Affected Versions | Confirmed on 2.9.28 (latest as of discovery); earlier versions likely affected |
| Fixed Version | 2.9.30.1 (hotfix) |
| CVSS 3.1 Score | 6.1 (Medium) |
| CVSS 3.1 Vector | AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N |
| Authentication Required | None (Unauthenticated) |
| User Interaction | Required (victim must visit attacker-controlled page or click crafted link) |
| Discovered By | Anthony Cihan — Obviam |
| Discovery Date | 2026-03-04 |
| Disclosure Date | 2026-03-18 |
| CVE ID | CVE-2026-4406 |
The Gravity Forms plugin for WordPress (tested through version 2.9.28) is vulnerable to unauthenticated reflected cross-site scripting (XSS) via the form_ids parameter in the gform_get_config AJAX action. The vulnerability exists because user-supplied form_ids values are reflected verbatim in the server's HTTP response without any sanitization, encoding, or output escaping. The response is served with a Content-Type: text/html; charset=UTF-8 header, which causes the browser to parse and render the reflected content as HTML, including any injected script elements.
An unauthenticated attacker can exploit this vulnerability to execute arbitrary JavaScript in the context of the target WordPress site's origin. Because the gform_get_config action requires a valid config_nonce, and this nonce is publicly embedded in the HTML source of every page that loads a Gravity Forms form, an attacker can trivially obtain a valid nonce by requesting any public-facing page on the target site before constructing the exploit request.
Successful exploitation allows an attacker to steal session cookies, perform actions on behalf of authenticated users (including WordPress administrators), redirect users to malicious sites, deface page content, or establish persistent access through administrative account creation.
POST /wp-admin/admin-ajax.php
gform_get_config
The args POST parameter accepts a JSON object containing a form_ids array. Values in this array are used as object keys in the JSON response structure without sanitization:
{"form_ids":["ATTACKER_CONTROLLED_VALUE"]}
The server processes the form_ids values and reflects them as JSON keys inside the response body. The response is wrapped in HTML comment markers and served as text/html:
Content-Type: text/html; charset=UTF-8
<!-- gf:json_start -->{"success":true,"data":{"common":{"form":{"pagination":{"ATTACKER_CONTROLLED_VALUE":null}}}}}<!-- gf:json_end -->
form_ids values are not validated as integers, sanitized, or filtered. The server accepts arbitrary string content including HTML tags and event handlers.form_ids values are reflected in the response body without HTML entity encoding. Characters such as <, >, ", and ' pass through unmodified.Content-Type: text/html; charset=UTF-8, which instructs the browser to parse the response body as HTML. Any HTML tags within the reflected value are instantiated and rendered by the browser's HTML parser.config_nonce required by the gform_get_config action is embedded in the JavaScript configuration object (gform_theme_config) on every page that loads a Gravity Forms form. This nonce is identical across all pages and is not bound to a specific user session, making it trivially obtainable by unauthenticated users.This vulnerability is the result of multiple contributing weaknesses that chain together to produce the exploitable condition. While CWE-79 is the primary classification for CVE reporting purposes, the full chain documents how each failure compounds to enable exploitation.
CWE-20 CWE-116 CWE-838 CWE-79
Improper Input → Improper Encoding → Inappropriate Encoding → Cross-Site
Validation or Escaping of Output for Output Context Scripting (XSS)
[EXPLOITABLE]
form_ids accepts Reflected values are JSON data served as Browser parses
arbitrary strings not HTML-entity text/html instead of injected HTML tags
instead of integers encoded in response application/json and executes JS
Role: Root enabler — allows malicious data to enter the processing pipeline.
The form_ids parameter in the args JSON object is expected to contain numeric form identifiers but accepts arbitrary string input without any validation. No type checking (intval()), no regex filtering (^[0-9]+$), no whitelist comparison against known form IDs, and no length restrictions are applied.
Evidence: The server accepts and processes form_ids values containing HTML tags, JavaScript event handlers, and arbitrary Unicode without rejection.
{"form_ids":["<svg onload=alert(1)>"]} ← Accepted
{"form_ids":["3"]} ← Expected
Role: Core vulnerability — the direct cause of the XSS condition.
When the server constructs the JSON response containing the form_ids values, it does not apply HTML entity encoding to the output. Characters with special meaning in HTML (<, >, ", ', &) pass through unmodified into the response body. The PHP functions htmlspecialchars(), esc_html(), wp_json_encode() with JSON_HEX_TAG, or equivalent output encoding functions are not applied to the form_ids values before they are written into the response.
Evidence: The literal string <svg onload=alert(document.domain)> appears in the response body byte-for-byte identical to the input, rather than as <svg onload=alert(document.domain)>.
Role: Context escalation — transforms a data reflection issue into executable code injection.
The response body contains JSON-structured data but is served with Content-Type: text/html; charset=UTF-8. This Content-Type declaration instructs the browser's HTML parser to process the entire response body as an HTML document. If the response were served as application/json, the browser would render the response as plain text and no HTML parsing would occur — the injected tags would be displayed as literal text rather than instantiated as DOM elements.
Evidence:
Content-Type: text/html; charset=UTF-8 ← Actual (enables HTML parsing)
Content-Type: application/json ← Expected (would prevent exploitation)
The X-Content-Type-Options: nosniff header is present but irrelevant because the server is explicitly declaring text/html — there is no MIME type sniffing to prevent.