The Exploit
An unauthenticated attacker can inject arbitrary JavaScript into a form's subject field that will execute in the browser of any user who accesses the published form.
POST /wp-json/fluent-form/v1/forms HTTP/1.1
Host: target.example.com
Content-Type: application/json
{
"submit_button": {
"settings": {
"button_ui": {
"text": "<img src=x>"
}
}
}
}
When a visitor loads the page containing the form, the injected payload in button_ui.text executes without sanitization, breaking out of the attribute context and executing the malicious JavaScript. The attacker observes the JavaScript alert box pop up in the visitor's browser, confirming code execution in the page context where the form is embedded.
What the Patch Did
Before
$buttonText = self::getAttributeWithShortcode(
[
'button_text' => ArrayHelper::get($form->submit_button, 'settings.button_ui.text')
],
$form
);
ArrayHelper::set($form->submit_button, 'settings.button_ui.text', $buttonText);
After
$form->image_preloads = $imagePreloads;
return $form;
The patch removed the call to self::getAttributeWithShortcode() entirely, along with the method definition itself (lines 1160–1210 of the old code). This method was processing user-supplied form attributes through WordPress shortcode expansion without output escaping. The security control added is implicit: by removing dynamic shortcode processing from user-controlled fields, the plugin eliminated the code path that could interpret attacker input as executable directives. The fixed code now passes form data directly to the response without intermediate transformation.
Root Cause
CWE-79: Improper Neutralization of Input During Web Page Generation (Cross-site Scripting)
The vulnerability flows from the submit_button.settings.button_ui.text parameter, which accepts unauthenticated input during form submission. This value is passed to getAttributeWithShortcode(), which performs WordPress shortcode replacement and variable expansion on the raw string. The processed output is then stored in $form->submit_button and returned to the client-side form renderer without HTML escaping. When the form markup is generated and inserted into the page DOM, the attacker's payload (e.g., <img src=x>) is not enclosed in quotes or HTML-entity-encoded, allowing the browser to interpret the injected event handler as executable script. The attacker crosses the trust boundary between user input (untrusted) and rendered page content (should be safe).
Why It Works
The load-bearing line is the complete removal of the getAttributeWithShortcode() method invocation and its definition. If this method remained but added wp_kses_post() or esc_attr() to its return value, the exploit would still fail because the payload would be neutralized before storage or output. However, the engineer did not add escaping; they removed the entire processing pipeline. This is a valid (and sometimes cleaner) fix when the processing itself serves no legitimate purpose. The shortcode parser was originally designed to allow form builders to use WordPress shortcodes in field labels or button text—a feature that creates an inherent tension with security. By removing it, the patch eliminates the attack surface rather than trying to sanitize shortcodes after parsing. The trade-off is that form fields no longer support dynamic content via shortcodes, but the security gain outweighs this feature loss.
Hardening Checklist
-
Apply
esc_attr()to all form label, button text, and placeholder attributes before rendering: Use WordPress's attribute escaping function on any user-supplied text inserted into HTML attributes. This prevents breaking out of the attribute context into the tag body. -
Never pass unsanitized user input to
do_shortcode()or custom shortcode parsers: If shortcode expansion is necessary, applywp_kses_post()after expansion to strip dangerous tags, or explicitly whitelist shortcodes allowed in form fields using theshortcode_atts_filter hooks. -
Run a static analysis check for unescaped outputs in form renderers: Use tools like PHPCS with the WordPress security ruleset (
wpcs) to catch calls to string concatenation or array assignment involving user input without a known escaping function in the same expression. -
Audit all calls to
ArrayHelper::get()on user-supplied arrays: Treat the return values as untrusted; do not pass them directly to rendering functions without escaping. Mark the function calls with inline comments documenting which escaping function applies downstream.
References
- https://nvd.nist.gov/vuln/detail/CVE-2024-10646