HTTP Security Headers: The Complete Configuration Guide
Configure CSP, HSTS, X-Frame-Options, Permissions-Policy, and Referrer-Policy correctly with ready-to-use nginx, Express, and Next.js configuration examples.
HTTP Security Headers: The Complete Configuration Guide
HTTP security headers are one of the highest-leverage defenses available to web applications. A few lines of configuration protect against entire categories of attack — clickjacking, XSS, protocol downgrade attacks, and data leakage via referrers. Yet misconfigured or missing headers remain among the most common findings in web application security assessments.
This guide covers every header that matters, explains what it does, and provides correct configuration for nginx, Express, and Next.js.
Content-Security-Policy (CSP)
CSP is the most powerful security header and the most complex to configure correctly. It instructs the browser to only execute scripts, load styles, and make connections to sources you explicitly list, making XSS exploitation dramatically harder even when an injection vulnerability exists.
A solid baseline CSP for a Next.js application:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{RANDOM_NONCE}';
style-src 'self' 'nonce-{RANDOM_NONCE}';
img-src 'self' data: https://cdn.yourapp.com;
font-src 'self' https://fonts.gstatic.com;
connect-src 'self' https://api.yourapp.com;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
upgrade-insecure-requests;
Key directives to understand:
default-src 'self'— fallback for all resource types not explicitly listed; restricts to same origin.script-src 'nonce-...'— only scripts with a matching nonce attribute execute. Generate a fresh cryptographic nonce per request. Never useunsafe-inlineorunsafe-evalin production.frame-ancestors 'none'— supersedesX-Frame-Options; prevents your page from being embedded in any frame.base-uri 'self'— prevents<base>injection, which can redirect all relative URLs.upgrade-insecure-requests— tells the browser to load all subresources over HTTPS even if referenced via HTTP.
CSP in Next.js with nonces
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import crypto from 'crypto';
export function middleware(request: NextRequest) {
const nonce = crypto.randomBytes(16).toString('base64');
const csp = [
`default-src 'self'`,
`script-src 'self' 'nonce-${nonce}'`,
`style-src 'self' 'nonce-${nonce}'`,
`img-src 'self' data: blob:`,
`connect-src 'self'`,
`frame-ancestors 'none'`,
`base-uri 'self'`,
`form-action 'self'`,
].join('; ');
const response = NextResponse.next();
response.headers.set('Content-Security-Policy', csp);
response.headers.set('x-nonce', nonce); // Pass to layout via headers
return response;
}
HTTP Strict Transport Security (HSTS)
HSTS tells browsers to always use HTTPS for your domain, even if a user types http:// or clicks an HTTP link. It eliminates SSL-stripping attacks.
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
max-age=31536000— cache this instruction for one year (the standard recommendation).includeSubDomains— apply to all subdomains. Add this only once you are sure every subdomain is HTTPS-capable.preload— opt into the browser preload list. Browsers ship with a hardcoded list of HSTS domains that never make an initial HTTP request. Submit your domain at hstspreload.org after adding this directive.
HSTS should only be sent over HTTPS responses. Sending it over HTTP is meaningless because an attacker can strip it.
X-Frame-Options
This legacy header prevents your page from being framed, defending against clickjacking. It is superseded by CSP frame-ancestors but should still be set for older browser compatibility.
X-Frame-Options: DENY
Use DENY unless you explicitly need to frame pages within your own origin, in which case use SAMEORIGIN. The third option ALLOW-FROM is not supported in modern browsers; use CSP frame-ancestors for per-origin exceptions.
X-Content-Type-Options
Prevents browsers from MIME-sniffing responses away from the declared Content-Type. This stops attacks where a server returns a file the browser sniffs as JavaScript and executes.
X-Content-Type-Options: nosniff
Always set this. It has no downside.
Permissions-Policy
Formerly Feature-Policy, this header disables browser APIs your application does not use. Restricting access to camera, microphone, and geolocation limits the damage from XSS.
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=(), usb=()
Only list APIs you actually need. If your app uses geolocation legitimately:
Permissions-Policy: camera=(), microphone=(), geolocation=(self), payment=()
Referrer-Policy
Controls how much URL information is included in the Referer header when navigating away from your page. Sensitive URL path information (user IDs, tokens in query strings, internal paths) can leak to third parties via referrer.
Referrer-Policy: strict-origin-when-cross-origin
This is the recommended default. It sends the full URL for same-origin requests (useful for analytics) and only the origin for cross-origin requests (prevents path leakage).
Other useful values:
no-referrer— send nothing; most private, may break some analytics.same-origin— send full URL only to same origin, nothing cross-origin.strict-origin— send only origin (no path) on all requests, and nothing when downgrading to HTTP.
Complete Configuration Examples
nginx
server {
# HSTS — only in the HTTPS server block
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
# Framing
add_header X-Frame-Options "DENY" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'; upgrade-insecure-requests;" always;
# MIME sniffing
add_header X-Content-Type-Options "nosniff" always;
# Referrer
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# Permissions
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()" always;
# Remove information-leaking headers
server_tokens off;
more_clear_headers Server;
more_clear_headers X-Powered-By;
}
Express (Node.js)
Use the helmet package, which sets sensible defaults for all the headers above:
import helmet from 'helmet';
import crypto from 'crypto';
app.use((req, res, next) => {
res.locals.nonce = crypto.randomBytes(16).toString('base64');
next();
});
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", (req, res) => `'nonce-${(res as any).locals.nonce}'`],
styleSrc: ["'self'", (req, res) => `'nonce-${(res as any).locals.nonce}'`],
imgSrc: ["'self'", 'data:'],
connectSrc: ["'self'"],
frameAncestors: ["'none'"],
baseUri: ["'self'"],
formAction: ["'self'"],
upgradeInsecureRequests: [],
},
},
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true,
},
referrerPolicy: { policy: 'strict-origin-when-cross-origin' },
permittedCrossDomainPolicies: false,
}));
Next.js (next.config.ts)
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
async headers() {
return [
{
source: '/(.*)',
headers: [
{
key: 'Strict-Transport-Security',
value: 'max-age=31536000; includeSubDomains; preload',
},
{
key: 'X-Frame-Options',
value: 'DENY',
},
{
key: 'X-Content-Type-Options',
value: 'nosniff',
},
{
key: 'Referrer-Policy',
value: 'strict-origin-when-cross-origin',
},
{
key: 'Permissions-Policy',
value: 'camera=(), microphone=(), geolocation=(), payment=()',
},
],
},
];
},
};
export default nextConfig;
For CSP with nonces in Next.js, use middleware.ts as shown in the CSP section above, since next.config.ts headers cannot include per-request dynamic values.
Verifying Your Headers
After deployment, verify headers are correct:
curl -s -I https://yourapp.com | grep -iE "strict-transport|content-security|x-frame|x-content-type|referrer|permissions"
Online tools like securityheaders.com provide a graded report. ShipSafer's security headers scanner checks all of these automatically and flags missing or misconfigured values.
Common Mistakes to Avoid
- CSP with
unsafe-inline— negates XSS protection entirely. Use nonces. - HSTS without
includeSubDomainswhen subdomains exist — attackers can strip HTTPS on subdomains and steal cookies. - Not setting
alwaysflag in nginx — withoutalways, headers are only added to 200 responses, not 4xx/5xx pages. - Setting HSTS over HTTP — the browser ignores it, and you may confuse yourself thinking it is active.
- Omitting
Vary: Originon dynamic CORS headers — can cause CDN cache poisoning.
Security headers require minimal ongoing maintenance once set correctly. Automated scanning ensures they do not regress during deployments.