The Exploit
An unauthenticated attacker can inject arbitrary JavaScript into the donor wall shortcode by crafting a donation with a malicious name field. The payload executes in the browser of any visitor who views the page, provided avatars are enabled in WordPress settings.
Step 1: Inject the payload via donation submission
POST /wp-json/give/v2/donations HTTP/1.1
Host: target.local
Content-Type: application/json
{
"firstName": ""><script>alert('XSS')</script><img data-x=\"",
"lastName": "donor",
"email": "[email protected]",
"amount": "10.00",
"paymentMethod": "offline"
}
The attacker submits a donation form (or uses the REST API directly) with a first name containing a script tag. The firstName value flows into the donor record's name field without sanitization. WordPress stores the raw payload in the database.
Step 2: Trigger execution on any page using the donor wall shortcode
GET /page-with-donor-wall/ HTTP/1.1
Host: target.local
When any user visits a page displaying [give_donor_wall], the plugin renders donor avatars. The stored name value is inserted into an HTML alt attribute without escaping. The injected <script> tag closes the alt attribute and executes.
The attacker observes the JavaScript alert fire in their browser console. Any visitor to that page will execute the attacker's payload, allowing credential theft, session hijacking, or malware injection.
What the Patch Did
Before
<img src='$imageUrl' alt='$alt' style='height: {$avatarSize}px;'/>
After
<img src='" . esc_url($imageUrl) . "' alt='" . esc_attr($alt) . "' style='" . esc_attr('height: ' . $avatarSize . 'px;') . "'/>
The patch wraps all user-controlled variables ($alt, $imageUrl, $avatarSize, and $donor->name) with WordPress escaping functions. The critical additions are esc_attr() for HTML attributes and esc_url() for URLs. esc_attr() converts special characters to HTML entities (< becomes <, " becomes "), making it impossible for an attacker to break out of the attribute context and inject tags or event handlers. esc_url() validates and sanitizes URLs to prevent javascript: protocol attacks in src attributes.
Root Cause
CWE-79: Improper Neutralization of Input During Web Page Generation (Cross-site Scripting)
The donor wall shortcode template receives donation data (specifically the name field) from the WordPress database. This data originates from untrusted user input submitted via the donation form. The template interpolates $donor->name and other variables directly into HTML attribute values (e.g., the alt attribute) without calling any escaping function. The browser parses the resulting HTML and, if the name contains quote characters or tag-like syntax, interprets it as HTML markup rather than plain text. The vulnerability crosses the trust boundary between the database (which stores raw user input) and the rendering layer (which should treat all output as potentially hostile).
Why It Works
The load-bearing line is esc_attr(). If you removed only that function and left the rest of the patch in place—still concatenating strings, still calling esc_url() on URLs—an attacker could still break out of the alt attribute with a double-quote, then inject an onload event handler or use the <img src="x"> vector.
The engineer added esc_url() for defence-in-depth: even if someone later refactors the code and forgets to escape the alt text, a malicious src URL is still neutered. The style attribute escaping (esc_attr() on the combined style string) prevents CSS injection or event handler injection via attributes like style="background:url('javascript:...)')". Together, these functions ensure that no matter which attribute an attacker targets, the output is treated as data, not markup or code.
Hardening Checklist
-
Audit all
echoand string interpolation in template files. Use a grep pattern likeecho\s+\$(?!.*esc_|.*absint|.*intval)to find unescaped variable output. Pair with a linter or PHPCS rule forWordPress.Security.EscapeOutput. -
Apply context-aware escaping functions at the point of output. For HTML attributes, use
esc_attr(). For URLs in href or src, useesc_url(). For HTML body content, usewp_kses_post(). Never escape at the input layer and assume the value is safe by the time it reaches the template. -
Never trust data from the database. Treat all user-submitted fields (
name,email, custom fields) as untrusted, even after storage. The database is a data store, not a security boundary. -
Enable and configure
wp_kses_post()orsanitize_text_field()during form processing. While this does not replace output escaping, it reduces the attack surface by stripping dangerous tags at submission time. Usesanitize_text_field()for plain-text fields like donor names. -
Run WordPress security-focused PHPCS sniffs in CI. The
WordPress-Securityruleset catchesEscapeOutputandValidatedSanitizedInputviolations. Enforce this in pull request checks.
References
- https://nvd.nist.gov/vuln/detail/CVE-2025-13206