Reflected XSS Demonstration
"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.
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:
GET /track-visit requestUser-Agent or Referer header value to:<img src=x onerror=alert('XSS')>// 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
});
node server.jsconst 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'));
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:
Content-Security-Policy as defense in depth against
any sanitization gaps.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.