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.

11 min read
ibrahimsql
2,189 words

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.

CarrierConsumerWhere it fires
Uploaded file nameAdmin file listFile manager page
Image EXIF fieldsAdmin image galleryGallery detail page
Profile fields set via APIAdmin dashboardUser detail view
Address or note fieldsSupport agentTicket view
CSV export headersFinance analystSpreadsheet viewer
Push notification titlesEnd userNotification 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#

  1. Upload a file with markup in the name in a lab build.
  2. View the file list as the admin role.
  3. 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#

  1. Create a lab image with markup in the description field.
  2. Let the app read and display the metadata.
  3. 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#

  1. Store the value via the API the app itself uses.
  2. View the field in the mobile client.
  3. View the field in the web admin panel.
  4. 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:

  1. Encode at the sink. Every field that renders into HTML gets HTML-encoded, no matter where it came from.
  2. 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#

SignalWhere to look
Queue records with angle bracketsStored rows in the table
OPTIONS or GETs to a collector hostProxy history and server logs
Admin with a new script contextCSP report endpoint
Export files with formula prefixesFinance queue
Repeated 403s on admin endpointsThe 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#

  1. Find an upload form.
  2. Rename a lab file to contain markup.
  3. Upload it as a low-privilege user.
  4. Open the file list as the admin.
  5. Record whether the name rendered.

Walkthrough 2: metadata path#

  1. Edit the lab image description with markup.
  2. Upload it through the normal path.
  3. View the gallery detail as the admin.
  4. Record whether the metadata rendered.

Walkthrough 3: API path#

  1. PUT a profile field with markup.
  2. View it in the mobile app.
  3. View it in the web admin panel.
  4. Record both renderers and their encoding.

Reporting Shape#

FieldExample
CarrierUploaded file name
Stored recordFile ID 4417
SinkAdmin file list
ObservedName rendered as markup
EvidenceScreenshot of the list, trimmed
FixEncode 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#

Triage Table for Renderers#

RendererEncodingNote
Mobile appText viewSafe shape
Web listEncoded HTMLSafe shape
Web detailRaw HTMLFinding
Email bodyRaw HTMLFinding
Export CSVEvaluatedDifferent 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:

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#

SurfaceCarrierConsumer
Ticket queueTicket textAgent console
Push notificationsNotification bodyEnd-user device
Admin exportsRow labelsSpreadsheet
Shared searchResult labelsEveryone
Webhook payloadsBody fieldsDownstream 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.

ClientUpload nameEXIF fieldAPI profile
Web admincheckcheckcheck
Mobilen/acheckcheck
Email templatesn/an/acheck
Exportscheckcheckn/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#

  1. For every stored field, ask which other clients render it.
  2. 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.
---
Share this post:

What do you think?

React to show your appreciation

Related Posts