Preventing XSS in modern JavaScript applications
React escapes text by default, which handles most XSS. The remaining cases are the ones people write deliberately.
The security headers worth setting on every site are Content-Security-Policy, Strict-Transport-Security, X-Content-Type-Options, Referrer-Policy, Permissions-Policy and frame protection via CSP frame-ancestors. CSP is by far the most valuable and the only one requiring real configuration effort.
| Header | Protects against | Safe starting value |
|---|---|---|
| Content-Security-Policy | XSS, injection, data exfiltration | Nonce-based, strict-dynamic |
| Strict-Transport-Security | Protocol downgrade, cookie interception | max-age=31536000; includeSubDomains |
| X-Content-Type-Options | MIME sniffing | nosniff |
| Referrer-Policy | URL leakage to third parties | strict-origin-when-cross-origin |
| Permissions-Policy | Unwanted API access by embeds | camera=(), microphone=(), geolocation=() |
| CSP frame-ancestors | Clickjacking | 'none' or 'self' |
// next.config.ts
const securityHeaders = [
{ key: "Strict-Transport-Security", value: "max-age=31536000; includeSubDomains" },
{ key: "X-Content-Type-Options", value: "nosniff" },
{ key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
{ key: "Permissions-Policy", value: "camera=(), microphone=(), geolocation=(), browsing-topics=()" },
];
export default {
async headers() {
return [{ source: "/:path*", headers: securityHeaders }];
},
};Generate a random nonce per request, include it in the policy and on every script tag, and use strict-dynamic so nonce-loaded scripts can load their own dependencies.
// middleware.ts
const nonce = Buffer.from(crypto.randomUUID()).toString("base64");
const csp = [
`default-src 'self'`,
`script-src 'self' 'nonce-${nonce}' 'strict-dynamic'`,
`style-src 'self' 'nonce-${nonce}'`,
`img-src 'self' data: https:`,
`font-src 'self'`,
`connect-src 'self'`,
`object-src 'none'`,
`base-uri 'none'`,
`frame-ancestors 'none'`,
`upgrade-insecure-requests`,
].join("; ");No measurably. They add a few hundred bytes per response. CSP has a trivial parsing cost that is not observable.
No. The browser XSS auditors it controlled have been removed, and in some configurations the header introduced its own issues. Use CSP instead.
frame-ancestors in CSP is the modern mechanism and is more flexible. Sending X-Frame-Options as well is harmless for very old clients.
It controls which browser features your page and its embedded frames may use — camera, microphone, geolocation and others. Denying what you do not use limits what a compromised embed can request.
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.
React escapes text by default, which handles most XSS. The remaining cases are the ones people write deliberately.
Certificates are free and automatic now. The remaining failures are almost all renewal automation that silently stopped working.
Not a compliance document. This is the list we actually work through before a client site handles its first real user.
No spam. Just the occasional case study and craft breakdown.