Writeup
The challenge had a stored XSS in the testimonials feed.
The application sanitized the testimonial body, but rendered the author display name directly with innerHTML:
nameDiv.innerHTML = t.user_name;Since the display name was user-controlled, I stored the payload in my profile name and then submitted a testimonial. When the feed rendered, the browser parsed my display name as HTML and executed the SVG payload.
Payload:
<svg><animate onbegin=top[`al`+`ert`]`1` attributeName=x dur=1s>Root Cause
The vulnerable sink was the display-name renderer:
nameDiv.innerHTML = t.user_name;The testimonial content was sanitized:
textDiv.innerHTML = DOMPurify.sanitize(t.content);So the issue was not a DOMPurify bypass. The payload simply avoided the sanitized field and entered through another attacker-controlled value: the profile display name.
Payload Breakdown
The interesting part of the solution was the payload:
<svg><animate onbegin=top[`al`+`ert`]`1` attributeName=x dur=1s>This uses SVG animation behavior to get automatic JavaScript execution.
The <animate> element supports the onbegin event. When the SVG animation starts, the browser fires onbegin, which executes the JavaScript inside it.
This means the payload does not need user interaction. Once the malicious display name is inserted into the DOM through innerHTML, the browser creates the SVG nodes, starts the animation, and triggers the handler.
The JavaScript part is:
top[`al`+`ert`]`1`This is equivalent to:
top.alert(1)but written in a less obvious form.
Breaking only the hard part down:
`al` + `ert`evaluates to:
"alert"So:
top[`al`+`ert`]becomes:
top["alert"]which resolves to the alert function on the top window.
Then the final template literal:
`1`is used as a tagged template call:
top[`al`+`ert`]`1`So the function is called without the classic alert(1) syntax.
In practice, the browser executes an alert when the SVG animation begins.
Why It Worked
The payload worked because the browser interpreted the stored display name as HTML.
The dangerous flow was:
profile display name→ stored server-side→ returned as t.user_name→ inserted with innerHTML→ parsed as SVG→ animate starts→ onbegin executes JavaScriptThe application protected t.content, but not t.user_name.
Steps to Reproduce
- Register or log in.
- Go to the profile page.
- Set the display name to:
<svg><animate onbegin=top[`al`+`ert`]`1` attributeName=x dur=1s>- Save the profile.
- Go to the testimonials page.
- Submit any testimonial.
- When the feed renders, the stored display name is parsed as HTML.
- The SVG animation starts and the payload executes.
Impact
This is stored XSS.
The payload is saved in the attacker’s profile name and executes for users who view the testimonials feed. An attacker could execute arbitrary JavaScript in the application origin, perform same-origin actions as the victim, modify the page, inject phishing UI, or interact with same-origin APIs using the victim’s browser context.
Fix
The display name should be rendered as text, not HTML.
Replace:
nameDiv.innerHTML = t.user_name;with:
nameDiv.textContent = t.user_name;Sanitizing the display name with DOMPurify would also reduce risk, but display names do not need HTML support. textContent is the safer fix.
BOOM
