Zero-Interaction XSS: Payloads in Metadata, Filenames, and API Responses
XSS vectors that trigger without a click: metadata, filenames, and API responses rendering unsanitized content.
Zero-Interaction XSS: Payloads in Metadata, Filenames, and API Responses#
Why this topic matters: the XSS that fires without anybody clicking a link reaches exactly the users who ignore suspicious links, usually the admins and the moderators.
Ethics and scope: confirm the sink in a lab before a client demo, and never use a real callback that harvests more than a proof header. The underlying delivery notes live in XSS types compared.
What Zero Interaction Means#
A zero-interaction payload is one that fires when a different user opens an ordinary screen. Nobody receives a link to click. The payload travels inside stored data: a file name, an image header, a profile field, an API response.
| Carrier | Consumer | Where it fires |
|---|---|---|
| Uploaded file name | Admin file list | File manager page |
| Image EXIF fields | Admin image gallery | Gallery detail page |
| Profile fields set via API | Admin dashboard | User detail view |
| Address or note fields | Support agent | Ticket view |
| CSV export headers | Finance analyst | Spreadsheet viewer |
| Push notification titles | End user | Notification center |
Filename Carriers#
An upload form that stores the original file name hands the application a free-form string. If the file list renders that string as HTML, the name is the payload.
Upload: "><svg onload=alert(1)>.png Result: <img src="x" onerror="..."><svg onload=alert(1)>.png
The check#
- Upload a file with markup in the name in a lab build.
- View the file list as the admin role.
- Note whether the name was encoded, stripped, or rendered.
Expected result for a safe app: the name shows as text, inert.
Why it reaches admins#
Admins review queues. The queue is the sink. The attacker does not deliver a link; the attacker files a ticket or uploads a file, and the admin's normal workflow delivers it.
Metadata Carriers#
Images and documents carry fields the user rarely sees: camera model, author, title, comments. If the app extracts and displays them without encoding, the field is the payload.
EXIF ImageDescription: <script>tag-body</script>
The check#
- Create a lab image with markup in the description field.
- Let the app read and display the metadata.
- Check which viewer renders it raw.
Variants live in thumbnails, document properties, and audio tags. The carrier changes; the sink question is the same: does anything render this string as HTML?
API-Response Carriers#
APIs return data the front end decides how to render. The same field can be safe in one client and dangerous in another.
PUT /api/profile HTTP/1.1 {"name": "<img src=x onerror=alert(1)>"}
The check#
- Store the value via the API the app itself uses.
- View the field in the mobile client.
- View the field in the web admin panel.
- Compare the encoding of each renderer.
The mobile client usually treats the value as text. The admin panel often builds HTML client-side. The second renderer is the finding.
Stored Fields That Cross Roles#
Zero-interaction XSS is a subset of the bigger pattern in attacking secondary contexts: a low-privilege user writes, a high-privilege user reads. The payload does not need to beat any filter on the write path; it needs one renderer on the read path that treats data as markup.
CSP and Encoding#
Two defenses cover this class:
- Encode at the sink. Every field that renders into HTML gets HTML-encoded, no matter where it came from.
- Send a CSP that blocks inline script. The payload may render, but it does not execute.
Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'none'
Encoding removes the bug. CSP bounds the blast radius when a new sink ships anyway.
Detection Notes#
| Signal | Where to look |
|---|---|
| Queue records with angle brackets | Stored rows in the table |
| OPTIONS or GETs to a collector host | Proxy history and server logs |
| Admin with a new script context | CSP report endpoint |
| Export files with formula prefixes | Finance queue |
| Repeated 403s on admin endpoints | The reviewer testing the sink |
False Positives#
- The payload fired in your own session because you replayed the field as yourself.
- The field encodes correctly everywhere except a debug page.
- The file name renders raw only in one report export.
- The callback fired from your own karma queue, not from the target.
Verification Discipline#
- Two roles: a writer and a viewer. Neither is you.
- One stored record, one rendered screen, one screenshot of the sink.
- Note which encoding the sink used. "Rendered raw" is the claim; show the markup that proves it.
Limits of the Class#
- Some sinks only exist in specific clients; document the client.
- A CSP can mask the bug without fixing it.
- An exported payload that never lands in a renderer is a data-quality issue, not XSS.
- Out-of-band callbacks prove a browser fired the script, not which user account did the viewing.
Triage Walkthrough#
Walkthrough 1: upload path#
- Find an upload form.
- Rename a lab file to contain markup.
- Upload it as a low-privilege user.
- Open the file list as the admin.
- Record whether the name rendered.
Walkthrough 2: metadata path#
- Edit the lab image description with markup.
- Upload it through the normal path.
- View the gallery detail as the admin.
- Record whether the metadata rendered.
Walkthrough 3: API path#
- PUT a profile field with markup.
- View it in the mobile app.
- View it in the web admin panel.
- Record both renderers and their encoding.
Reporting Shape#
| Field | Example |
|---|---|
| Carrier | Uploaded file name |
| Stored record | File ID 4417 |
| Sink | Admin file list |
| Observed | Name rendered as markup |
| Evidence | Screenshot of the list, trimmed |
| Fix | Encode at render, keep CSP |
When CSP Is the Wrong Fix#
CSP limits damage; it does not remove stored markup. If the same field is rendered in three different clients, encoding at the sink is the only fix that travels with the data. Note that in the report, because a reviewer will ask why both are recommended.
Pattern Behind the Pattern#
Every zero-interaction payload is the same sentence: "user A stored data; user B's browser rendered it as script." Once that sentence is true for one field, it is worth checking the sibling fields: the same queue, the same export, the same dashboard.
What to Test Next#
- Every place a stored name renders.
- Every export that ships a name into a spreadsheet.
- Every notification body that quotes a stored field.
- Every search result that echoes a stored value.
- Every API response that a second client renders.
Reading Order Around This Topic#
- The delivery differences: XSS types compared.
- The consumer-role pattern: attacking secondary contexts.
- Class map: web hacking 101.
- Injection siblings: SQL injection notes.
- Unicode normalization siblings: WAF and Unicode notes.
- WordPress variants of the same sinks: WordPress attack surface.
- Client-side data traps: prototype pollution notes.
- The API side: API access control test plan.
- Role-based access checks behind the sinks: BOLA test matrix.
- The proxy loop: Burp Suite basics.
- Traffic confirmation: Wireshark guide.
- Roadmap context: reading order.
Triage Table for Renderers#
| Renderer | Encoding | Note |
|---|---|---|
| Mobile app | Text view | Safe shape |
| Web list | Encoded HTML | Safe shape |
| Web detail | Raw HTML | Finding |
| Email body | Raw HTML | Finding |
| Export CSV | Evaluated | Different class |
The One Rule#
Encode at the sink, keep CSP as a seatbelt, and verify the encoding actually happens in every client that renders the field. The bug is not in the payload; the bug is in the renderer that trusts stored data.
A Trimmed Report Shape#
Title: Stored file name renders unescaped in admin file list Roles: user-a (writer), admin (viewer) Replay: upload file "q<svg onload=alert(1)>.png" as user-a; admin opens Files > Queue in the web console Observed: file list renders the name as live markup Impact: administrator session runs a script in the web console Fix: HTML-encode the file name at render time; enforce a CSP that blocks inline script Verification: re-upload, confirm the name shows as text
How to Write the Payload for the Lab#
Pick inert payloads first. A harmless <b> tag proves markup evaluation without executing anything. Only after the sink confirms markup evaluation do you add a tracker or an alert in the lab build. A report that starts with alert(1) invites nobody. A report that shows the rendered <b> tag speaks for itself.
Reading Order, Continued#
Once the sink class is clear, the natural next reads are:
- API role checks behind the same records: API access control test plan.
- The scanner's blind spot on these sinks: ZAP review notes.
- The class background: OWASP Top 10 notes.
- What exploit development looks like after reconnaissance: exploit development stages.
- The CSV twin of this class: attacking secondary contexts.
- What to do when the renderer is Next.js: noindex is not auth.
A Note on Out-of-Band Proof#
A callback server proves the browser, not the user. Record the exact queue record you seeded so the viewing admin is a fact you can point at, not a guess.
Where Else the Pattern Sits#
| Surface | Carrier | Consumer |
|---|---|---|
| Ticket queue | Ticket text | Agent console |
| Push notifications | Notification body | End-user device |
| Admin exports | Row labels | Spreadsheet |
| Shared search | Result labels | Everyone |
| Webhook payloads | Body fields | Downstream app |
Verification Cadence#
Run the upload, metadata, and API checks once per app release. Add a row per client. The matrix below is the durable artifact.
| Client | Upload name | EXIF field | API profile |
|---|---|---|---|
| Web admin | check | check | check |
| Mobile | n/a | check | check |
| Email templates | n/a | n/a | check |
| Exports | check | check | n/a |
A Small Payload Kit for the Lab#
markup probe: <b>x</b> attribute probe: " onmouseover="x comment break: --!><b>x</b> filename probe: file-<b>x</b>.png metadata probe: author:<b>x</b>
Inert markers first. One tracker per run. One collector path per report.
Two Checks That Pay for Themselves#
- For every stored field, ask which other clients render it.
- For every renderer, ask which encoding it applies.
Two questions, one afternoon, and most of this class stops being invisible.
The Discipline Behind the Payload#
The payload is the easy part. The discipline is the queue: write the field, wait for the human, record the exact client that rendered it raw, and keep the screenshot that shows the sink with the role of the viewer attached.
Edge Notes for the Report#
- Note whether the client was a browser, a mail reader, or a spreadsheet.
- Note whether the CSP was present and what it allowed.
- Note whether the field was editable again after the report, since fixed apps often stop encoding on edit paths only.
- Note whether exports were included in the fix or left out.
Those four notes turn a one-line finding into a finding the team can actually close.
Once More, Briefly#
The writer never has to click. The reader is the payload's trip. Record both sides of that trip, and the report stays short because the evidence is already attached.
Carrier to Sink Boundary#
attacker input --> stored field (filename, EXIF, API field) --> consumer screen --> sink | encoding or not
Zero-interaction XSS wins when the stored field crosses into a consumer that renders it as HTML without encoding. The attacker never sends a link; the admin workflow delivers it.
Detection#
- Upload a markup-laden filename in a lab build and view the admin list.
- Check EXIF description fields against the gallery renderer.
- Compare API-stored values across web admin, mobile, and export views.
- Note whether the field is re-encoded on edit paths.
Limitations#
- Some carriers are stripped by the upload pipeline; a payload that survives as text is closed.
- CSP reduces blast radius but does not fix the sink.
- Spreadsheet and PDF viewers have their own encoding rules; test each consumer.
Cikarilar#
- The reader is the trip; the writer never clicks.
- Filename, EXIF, and API fields are payload slots.
- Encoding belongs at render, not at input.
- Record both sides: stored value and rendering view.
What do you think?
React to show your appreciation