The Exploit
An authenticated WordPress user with subscriber-level access can inject arbitrary shortcodes into a review submission that will execute when the review post is rendered on the front end.
POST /wp-admin/admin-ajax.php HTTP/1.1
Host: target.local
Content-Type: application/x-www-form-urlencoded
Cookie: wordpress_logged_in=<subscriber_session>
action=wur_submit_review&post_id=42&xs_reviw_title=Test&xs_reviw_summery=[[gallery]]&xs_reviwer_name=Attacker&[email protected]&xs_reviwer_ratting=5&xs_reviw_summery=&security=<nonce>
When a visitor navigates to /reviews/test-review/ and the review post renders via the_content(), the double-bracket payload [[gallery]] survives sanitization, gets unwrapped by WordPress's strip_shortcodes() into a bare [gallery] tag, and fires — executing the gallery shortcode handler with attacker-controlled arguments. An attacker can chain this into RCE by targeting shortcodes registered by other plugins that accept file paths or callback functions.
What the Patch Did
Before
$postarr['post_content'] = isset($metaReviewData['xs_reviw_summery']) ? strip_shortcodes( sanitize_textarea_field($metaReviewData['xs_reviw_summery']) ) : '';
After
$postarr['post_content'] = isset($metaReviewData['xs_reviw_summery']) ? $this->wur_neutralise_shortcodes( wp_kses( wp_specialchars_decode( sanitize_textarea_field($metaReviewData['xs_reviw_summery']) ), array() ) ) : '';
The patch replaced the insufficient strip_shortcodes() call with a new custom method wur_neutralise_shortcodes() that escapes the bracket characters themselves — converting [ to [ and ] to ]. This is the canonical WordPress-grade defence against shortcode injection: it doesn't try to recognize and remove specific shortcode patterns; it neutralizes the metacharacters that form any shortcode syntax. The additional wp_kses(..., array()) call with an empty allowed-tags array strips all HTML/tags from the decoded input as a secondary control.
Root Cause
CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
The attack flows from the xs_reviw_summery POST parameter submitted to the AJAX handler. This user-supplied string is passed to sanitize_textarea_field() — which removes dangerous HTML tags but does not neutralize shortcode syntax — then to strip_shortcodes(), which is a single-pass regex that only removes shortcodes already registered at the time of execution. By wrapping the payload in double brackets ([[gallery]]), the attacker causes strip_shortcodes() to remove the outer layer, leaving [gallery] intact. This sanitized-but-weaponized content is stored in wp_posts.post_content via wp_insert_post(). When the review post renders via the_content() hook, WordPress's do_shortcode call processes the stored [gallery] tag, and the shortcode executes in the rendering context — crossing the trust boundary from stored user input to page output without output escaping.
Why It Works
The load-bearing line is $this->wur_neutralise_shortcodes(). Removing it would leave the attacker-controlled payload vulnerable: even with wp_kses(..., array()) present, the HTML stripping does not prevent shortcode execution. The bracket-escaping method is necessary and sufficient because it destroys the syntax required to invoke any shortcode, regardless of registration state or payload encoding. The wp_kses and wp_specialchars_decode layers are redundant defences: they ensure that if a shortcode somehow survives neutralization, it cannot be injected as an attribute or reanimated by decoding. The engineer added them to implement defence-in-depth — a shortcode escaper alone is correct, but layering HTML sanitization and decoding prevention catches mistakes in the primary escaper and future refactors.
Hardening Checklist
- Always escape for context, not for pattern recognition. Use
esc_attr(),wp_kses_post(), or custom escaping (like bracket entityification here) at output time; never rely onstrip_shortcodes()or regex-based removal at storage time. - Apply
wp_kses()with an empty allowed-tags array (array()) to user content destined for structured fields, not just free-form post content. This plugin stores reviews in post meta; it should have been sanitizing aswp_kses(..., array())from intake. - Validate input types explicitly before storage. The patch also added
is_numeric()and(float)casting for rating scores; do the same for any field that should have a fixed type — do not assumesanitize_text_field()will catch type confusion. - Use
wp_insert_post()capabilities checks, not just role checks. Even subscriber-level users should not be able to inject shortcodes; combinecurrent_user_can('edit_posts')with input validation to gate review submission at the capability level. - Audit all calls to
do_shortcode()in admin or public forms. Trace the source of the string argument backwards to its entry point; if it comes from user input, require explicit escaping or validation at the point of user intake, not at the rendering sink.
References
- https://nvd.nist.gov/vuln/detail/CVE-2026-103519