The Exploit
An unauthenticated attacker can inject WordPress shortcodes into donor names stored in the GiveWP database, which are then executed server-side when the donor wall or campaign donations block is rendered to any site visitor.
Store the payload by submitting a donation with a malicious donor name:
POST /wp-admin/admin-ajax.php HTTP/1.1
Host: target.wordpress.local
Content-Type: application/x-www-form-urlencoded
action=give_process_donation&donorName=[do_shortcode_injection_here]&donation_amount=10&[email protected]
Trigger execution by visiting any page that displays the donor wall or campaign donations block:
curl http://target.wordpress.local/campaigns/fundraiser-page/
The response body will contain the executed shortcode output embedded in the <strong> tag where the donor name should appear. If the attacker uses a shortcode like [site_url] or a custom plugin shortcode, the result renders in the page markup visible to all visitors. More critically, if a malicious shortcode exists on the site (injected via another plugin or custom code), the attacker can trigger arbitrary PHP execution.
What the Patch Did
Before
$html = ob_get_clean();
// Return only donor html.
if (
'<strong>' . esc_html(!$donation->isAnonymous ? $donation->donorName : __('Anonymous', 'give')) . '</strong>',
After
$html = ob_get_clean();
// Strip shortcodes to prevent execution of user-supplied shortcode syntax.
$html = strip_shortcodes($html);
// Return only donor html.
if (
'<strong>' . esc_html(!$donation->isAnonymous ? strip_shortcodes($donation->donorName) : __('Anonymous', 'give')) . '</strong>',
The patch adds two calls to WordPress's strip_shortcodes() function. In the donor wall output handler (class-give-donor-wall.php line 151), strip_shortcodes() is applied to the entire buffer before return. In the campaign donations block template (render.php line 93), it is applied directly to $donation->donorName prior to HTML escaping. The strip_shortcodes() function removes all shortcode syntax (anything matching [shortcode_name ... ] pattern) from a string without rendering it, preventing the WordPress do_shortcode pipeline from ever encountering the attacker's input.
Root Cause
CWE-94: Improper Control of Generation of Code ('Code Injection') — specifically, unsafe shortcode execution.
The donor name field accepts user input at the point of donation submission. This value is stored unfiltered in the database. When the donor wall or campaign donations block renders, the plugin calls WordPress rendering functions that eventually invoke do_shortcode() on output containing the donor name. WordPress's do_shortcode() function parses and executes all shortcode tags in its input. Because the plugin never called strip_shortcodes() before passing donor data to rendering contexts, any shortcode syntax in the donor name (e.g., [wp_mail_smtp_get_current_user] or custom plugin shortcodes) was executed in the rendering phase, crossing the trust boundary from user-supplied stored data into executable code.
Why It Works
The single load-bearing line is strip_shortcodes($html) in the donor wall buffer handler and strip_shortcodes($donation->donorName) in the campaign block. If either were removed, the bug remains fully exploitable — an attacker's shortcode injection still reaches the output and is still parsed by do_shortcode(). The subsequent esc_html() call does not prevent shortcode execution; it only HTML-encodes the characters after shortcodes are already processed. A shortcode like [my_malicious_function] would be stripped to an empty string before esc_html() ever sees it. Without strip_shortcodes(), esc_html('[my_malicious_function]') outputs the literal string [my_malicious_function], which looks safe in HTML but still triggers shortcode parsing earlier in the rendering pipeline. This is why the engineer added strip_shortcodes() at both locations: belt and suspenders defense ensures that no code path bypasses the removal, whether the output goes through the donor wall template or the campaign block template.
Hardening Checklist
-
Audit all user-input sinks for
do_shortcode()calls: Usewp-cli plugin search-replaceor grep to find every call todo_shortcode(),apply_filters()with shortcode-aware callbacks, and template rendering functions. For each, trace back to confirm the input has been explicitly stripped viastrip_shortcodes()or restricted to safe sources (e.g., post content created by admins only). -
Apply
strip_shortcodes()to all user-supplied fields before rendering: Treat donor names, testimonials, custom form fields, and any user-editable text as untrusted. Callstrip_shortcodes()immediately before output escaping (esc_html(),esc_attr()), not as an afterthought in a central template. -
Implement capability checks on donation form submission: Require
current_user_can('manage_options')or a custom capability for users who can edit their own donor name post-submission, and sanitize those edits withsanitize_text_field()followed bystrip_shortcodes(). -
Test shortcode injection in your test suite: Add a unit test that submits a donation with donor name
[do_shortcode]or[site_url]and asserts that the rendered output does not contain the shortcode's result or any executable syntax. -
Use
wp_kses_post()for rich text fields only: If donor names or testimonials genuinely need to support limited HTML, usewp_kses_post()which has a whitelist of safe tags — but still callstrip_shortcodes()first to be explicit about rejecting code injection vectors.
References
- https://nvd.nist.gov/vuln/detail/CVE-2025-66533