Security headers: which ones matter and what to set them to
Six headers, most of them one line each. They will not fix a vulnerability, but several of them turn one into a non-event.
Cross-site scripting occurs when untrusted input is executed as code in a user's browser. React escapes interpolated text automatically, so the real risks are dangerouslySetInnerHTML, unvalidated href and src attributes, injected script or style content, and DOM manipulation outside React. Sanitise HTML with a vetted library and add a Content-Security-Policy as defence in depth.
| Type | How it works | Example |
|---|---|---|
| Stored | Malicious input saved and served to others | A script in a profile bio |
| Reflected | Input echoed back in the response | A search term rendered unescaped |
| DOM-based | Client-side code writes untrusted data into the DOM | location.hash into innerHTML |
Stored XSS is the most damaging because it affects every visitor to the page. DOM-based XSS is the easiest to miss because the payload may never reach the server, so server-side filtering never sees it.
// Safe: React escapes interpolated values
<p>{userInput}</p>
// Unsafe: raw HTML injection
<div dangerouslySetInnerHTML={{ __html: userInput }} />
// Unsafe: javascript: URLs execute on click
<a href={userProvidedUrl}>Visit</a>
// Safe: validate the protocol first
const safeUrl = /^https?:\/\//.test(url) ? url : "#";Sanitise with a maintained allow-list library at render time, ideally on the server.
import DOMPurify from "isomorphic-dompurify";
const clean = DOMPurify.sanitize(userHtml, {
ALLOWED_TAGS: ["p", "b", "i", "em", "strong", "a", "ul", "ol", "li", "code"],
ALLOWED_ATTR: ["href", "title"],
ALLOWED_URI_REGEXP: /^https?:/i,
});A Content-Security-Policy tells the browser which sources may execute. With a strict nonce-based policy, an injected inline script has no valid nonce and is blocked — the XSS bug still exists, but it cannot execute.
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{random}' 'strict-dynamic';
style-src 'self' 'nonce-{random}';
img-src 'self' data: https:;
object-src 'none';
base-uri 'none';
frame-ancestors 'none'No. It escapes interpolated text, which removes the most common cases, but dangerouslySetInnerHTML, unvalidated URLs and direct DOM manipulation all bypass that protection.
Output, primarily. Sanitising only on input means data arriving through any other path is unprotected, and it destroys the original content irreversibly.
The first strict policy takes effort, especially with third-party scripts. Start in report-only mode, fix what it flags, then enforce. It is one of the highest-value security headers.
ROVQIX Engineering
Engineering team, ROVQIX
The ROVQIX engineering team builds and maintains web platforms, APIs and infrastructure for clients across SaaS, ecommerce and enterprise. These notes come out of real production work — deploys, incidents, migrations and audits.
ROVQIXdesigns and builds production web platforms — Next.js front ends, Node.js APIs and the infrastructure behind them. Tell us what you're building and we'll scope it with you.
Six headers, most of them one line each. They will not fix a vulnerability, but several of them turn one into a non-event.
SameSite=Lax handles most CSRF. 'Most' is doing real work in that sentence, and the gap is where the interesting attacks live.
The Top 10 is a list of categories, not bugs. Here is what each one looks like in a real codebase.
No spam. Just the occasional case study and craft breakdown.