The Exploit
An unauthenticated attacker can inject arbitrary JavaScript into WooCommerce email designer preview pages by crafting a malicious customer name during checkout. The payload is stored in the WooCommerce order object and executed whenever any user (including administrators) views the order via the Kadence plugin's preview or email rendering functionality.
POST /wp-json/wc/v3/orders HTTP/1.1
Host: target-wordpress.local
Content-Type: application/json
{
"billing": {
"first_name": "\"; alert('XSS'); var x=\"",
"last_name": "Test"
}
}
When an administrator or customer views the order in the Kadence email designer, the injected JavaScript executes in their browser context. The attacker observes the alert box firing, confirming script execution. More critically, the payload executes within the admin panel, allowing theft of session cookies, CSRF token manipulation, or silent redirection to a credential harvesting page.
What the Patch Did
Before
$scripts .= 'var pl_customer_first_name = "' . self::$current_order->get_billing_first_name() . '";';
$scripts .= 'var pl_customer_last_name = "' . self::$current_order->get_billing_last_name() . '";';
$scripts .= 'var pl_customer_full_name = "' . self::$current_order->get_formatted_billing_full_name() . '";';
$scripts .= 'var pl_customer_username = "' . Kadence_Woomail_Designer::get_username_from_id( $user_id ) . '";';
After
$scripts .= 'var pl_customer_first_name = "' . esc_js(self::$current_order->get_billing_first_name()) . '";';
$scripts .= 'var pl_customer_last_name = "' . esc_js(self::$current_order->get_billing_last_name()) . '";';
$scripts .= 'var pl_customer_full_name = "' . esc_js(self::$current_order->get_formatted_billing_full_name()) . '";';
$scripts .= 'var pl_customer_username = "' . esc_js( Kadence_Woomail_Designer::get_username_from_id( $user_id ) ) . '";';
The patch applied WordPress's esc_js() output escaping function to all dynamic values before concatenating them into JavaScript string literals. This function escapes characters with special meaning in JavaScript context—primarily quotes, backslashes, and newlines—preventing an attacker's quote characters from breaking out of the string and injecting arbitrary code. The security control is output escaping at the JavaScript sink, enforced by the esc_js() API.
Root Cause
This is Stored Cross-Site Scripting (CWE-79). Attacker-controlled data enters the system through WooCommerce order fields (billing first name, last name, formatted full name, and username) during checkout or order creation. These values flow from the database into class-kadence-woomail-preview.php lines 882–891 via WooCommerce getter methods like get_billing_first_name(). The code concatenates this untrusted data directly into a JavaScript string literal without escaping, crossing a trust boundary from the data tier into executable code context. When the email preview is rendered, the browser parses the malicious payload as JavaScript and executes it under the privileges of the logged-in administrator.
Why It Works
The load-bearing line is esc_js(self::$current_order->get_billing_first_name()). Without this single escaping function, an attacker's quote character (") terminates the string, and arbitrary JavaScript follows. With esc_js() applied, quotes are converted to escape sequences (\") that are rendered as literal characters within the string, not as syntax. The engineer wrapped all four name/username variables identically because each one is concatenated into a JavaScript string literal in the same pattern: var pl_customer_X = " + untrusted_data + ". Omitting esc_js() from even one variable (e.g., just the username) would leave the bug exploitable via that single unescaped sink.
Hardening Checklist
-
Apply output escaping context-specifically: Always use
esc_attr()for HTML attributes,esc_js()for JavaScript strings,esc_html()for HTML body content, andwp_json_encode()for JSON. Never trust that data is "safe" before escaping—escaping must occur at the sink, not at storage. -
Audit all template variable concatenation: Search the codebase for patterns like
'... "' . $var . '"'and'... \'' . $var . '\''in JavaScript and attribute contexts. These are high-risk concatenation patterns that demand immediate escaping review. -
Use static analysis for XSS detection: Integrate WordPress linting tools such as
wp-cli --since=5.0 lintor third-party SAST tools (e.g., SonarQube, Semgrep with WordPress rules) into CI/CD to catch unescaped variable output before release. -
Sanitize at input; escape at output: Even though this vulnerability was ultimately an escaping failure, implement
sanitize_text_field()on order metadata at the API boundary to prevent unexpectedly malicious characters from entering the database in the first place. -
Test with payload-bearing fixtures: Create automated tests that include customer names containing quotes, backslashes, and newlines, then verify the rendered JavaScript parses without syntax errors and does not execute injected code.
References
- https://nvd.nist.gov/vuln/detail/CVE-2025-13387