XSS & Content-Security-Policy
Cross-Site Scripting (XSS) occurs when an application receives untrusted data and sends it to the web browser without proper validation or escaping, allowing malicious client-side scripts to run in the user's browser context.
Core details
| Type | Origin | Execution Context | Primary Defense |
|---|---|---|---|
| Stored XSS | Database, persistent storage | Rendered to arbitrary users | Context-aware output encoding, HTML sanitization |
| Reflected XSS | HTTP query parameters, headers | Echoed back immediately in response | Context-aware output encoding, validation |
| DOM-based XSS | Client-side source (location.hash) | Dynamic write to unsafe DOM sink | Avoid dangerous sinks, sanitize, Trusted Types |
Dangerous DOM Sinks
A DOM sink is a browser API that can compile or execute strings passed to it.
- HTML Sinks:
element.innerHTML,element.outerHTML,document.write - Execution Sinks:
eval(),setTimeout(string),Function() - Navigation Sinks:
location.href,window.open(vulnerable tojavascript:scheme)
[!IMPORTANT] HTML escaping (e.g., converting
<to<) is only safe when rendering content inside standard HTML tags (e.g.,<div>userText</div>). If you are placing data inside attribute values (e.g.,<input value="userData">), CSS styles, or script blocks, you must use context-specific escaping.
Understanding
Content Security Policy (CSP)
CSP is a browser-enforced header that restricts where resources (scripts, stylesheets, images) can be loaded and executed from.
- Legacy CSP: Allowlist-based (e.g.,
script-src 'self' apis.google.com). Vulnerable to bypasses via JSONP endpoints on allowlisted domains. - Strict CSP: Nonce or hash-based. The server generates a unique, single-use random value (nonce) for every page render and attaches it to the header:
Content-Security-Policy: script-src 'nonce-rAnd0m123' 'strict-dynamic' https:;Only scripts with a matchingnonceattribute are executed by the browser.
Trusted Types
Trusted Types lock down the DOM sinks at the browser engine level. Instead of accepting raw strings, sinks like innerHTML only accept certified TrustedHTML objects created by pre-approved sanitization policies.
Senior understanding
Safe Dynamic Link Rendering (React Example)
When rendering links provided by users (e.g., user profiles or markdown links), you must validate the URL scheme to prevent javascript: and data: execution:
import React from 'react';
function sanitizeUrl(url: string): string {
const trimmed = url.trim().toLowerCase();
if (!trimmed) {
return 'about:blank';
}
// 1. Block dangerous schemes
if (trimmed.startsWith('javascript:') || trimmed.startsWith('data:') || trimmed.startsWith('vbscript:')) {
return 'about:blank';
}
// 2. Validate protocols (http, https, mailto, tel)
if (/^(https?|mailto|tel):/i.test(trimmed)) {
return url;
}
// Fallback to relative path or safe default
return '#';
}
interface UserLinkProps {
href: string;
label: string;
}
export function UserLink({ href, label }: UserLinkProps) {
const safeHref = sanitizeUrl(href);
return (
<a
href={safeHref}
target="_blank"
rel="noopener noreferrer" // Prevents tab-nabbing / reverse tabnabbing
>
{label}
</a>
);
}Diagram
See also
Mark this page when you finish learning it.
Spotted something unclear or wrong on this page?