XSS Security Lab

Reflected XSS Demonstration

Test Your Knowledge

VULNERABLE - Reflected XSS

Lab #7: Reflected XSS via Referer / User-Agent Header

"You came here from..." — a widget that trusts headers it never asked to be trustworthy

Some sites show a friendly "Recently visited from: example.com" or "Detected browser: Chrome" widget, built server-side from the Referer / User-Agent HTTP headers of the request. These values feel like "browser metadata" rather than "user input," so backends often echo them straight into an HTML response without sanitization.

That trust is misplaced. Both headers are fully attacker-controlled. Referer is just the URL of the previous page — an attacker can host a page at any URL they like and link a victim to your site from it. User-Agent can be edited freely by anyone using browser dev tools, curl, or — as in this lab — an intercepting proxy like Burp Suite. Neither is authenticated or validated by the browser in any way; the browser just reports whatever it's configured (or told) to report.

Requires the companion server (see below the code block) running on http://localhost:3000, with your browser's proxy pointed at Burp. Browsers block JavaScript from setting the real User-Agent / Referer headers directly — you have to edit them in Burp's Intercept tab after the request leaves the browser.

Click the button below to send a real request. Turn on Burp Proxy -> Intercept first, then:

  1. Click "Send Tracking Request"
  2. In Burp, find the intercepted GET /track-visit request
  3. Edit the User-Agent or Referer header value to:
    <img src=x onerror=alert('XSS')>
  4. Click Forward
  5. Watch the response render below — unsanitized, straight from the header
(no request sent yet)
// VULNERABLE CODE (frontend - client_lab7.js equivalent, inlined below)
async function sendTrackingRequest() {
    const res = await fetch('http://localhost:3000/track-visit');
    const html = await res.text();
    document.getElementById('referrerWidget').innerHTML = html;   // <-- unsanitized
}

// VULNERABLE CODE (backend - server.js, Node/Express)
app.get('/track-visit', (req, res) => {
    const ua  = req.headers['user-agent'] || '';
    const ref = req.headers['referer']    || '(none)';
    res.set('Access-Control-Allow-Origin', '*');
    res.send(
        '<p>Detected browser: ' + ua + '</p>' +
        '<p>Recently visited from: ' + ref + '</p>'
    );   // <-- headers written directly into HTML, no escaping
});
Companion server code (server.js) — run with node server.js
const http = require('http');

http.createServer((req, res) => {
    if (req.url === '/track-visit') {
        const ua  = req.headers['user-agent'] || '';
        const ref = req.headers['referer']    || '(none)';
        res.writeHead(200, {
            'Content-Type': 'text/html',
            'Access-Control-Allow-Origin': '*'
        });
        res.end(
            '<p>Detected browser: ' + ua + '</p>' +
            '<p>Recently visited from: ' + ref + '</p>'
        );
        return;
    }
    res.writeHead(404); res.end();
}).listen(3000, () => console.log('Vulnerable server on http://localhost:3000'));

The Fix

Treat every HTTP header the same way you'd treat a form field: as untrusted, attacker-controlled input. Never write a header value into the DOM or into an HTML response without proper handling:

// FIXED CODE (client-side)
function showReferrer() {
    const ref = document.referrer || '(direct visit — no referrer)';
    document.getElementById('referrerWidget').textContent =
        'Recently visited from: ' + ref;   // safe: rendered as text, not parsed
}

// FIXED CODE (server-side, Node/Express-style)
const escapeHtml = require('escape-html'); // or your framework's built-in escaper
app.get('/', (req, res) => {
    const ua = escapeHtml(req.headers['user-agent'] || '');
    res.send('<p>Detected browser: ' + ua + '</p>');
});

Additional hardening for header-driven features:

  • Never assume a header is "just metadata" — Referer, User-Agent, X-Forwarded-For, Origin, and every other header are all fully editable by the client sending the request.
  • If you only need the referrer's doindex for display (e.g. "via google.com"), parse it with the URL API and show only the validated hostname — never the raw string.
  • Use your templating engine's default auto-escaping for server-rendered pages instead of hand-building HTML strings from request data.
  • Set a strict Content-Security-Policy as defense in depth against any sanitization gaps.
  • Remember document.referrer can also be empty or spoofed to look "clean" by an attacker using a rel="noreferrer" link on the referring page — don't rely on it for any security decisions.