Security headers protect against common web vulnerabilities. Here's how to implement them.
Content-Security-Policy (CSP)#
1# Basic CSP
2Content-Security-Policy: default-src 'self';
3
4# More comprehensive
5Content-Security-Policy:
6 default-src 'self';
7 script-src 'self' 'unsafe-inline' https://cdn.example.com;
8 style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
9 img-src 'self' data: https:;
10 font-src 'self' https://fonts.gstatic.com;
11 connect-src 'self' https://api.example.com;
12 frame-ancestors 'none';
13 base-uri 'self';
14 form-action 'self';1// Express middleware
2import helmet from 'helmet';
3
4app.use(
5 helmet.contentSecurityPolicy({
6 directives: {
7 defaultSrc: ["'self'"],
8 scriptSrc: ["'self'", "'unsafe-inline'", "https://cdn.example.com"],
9 styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com"],
10 imgSrc: ["'self'", "data:", "https:"],
11 fontSrc: ["'self'", "https://fonts.gstatic.com"],
12 connectSrc: ["'self'", "https://api.example.com"],
13 frameAncestors: ["'none'"],
14 baseUri: ["'self'"],
15 formAction: ["'self'"],
16 },
17 })
18);
19
20// Next.js config
21// next.config.js
22const securityHeaders = [
23 {
24 key: 'Content-Security-Policy',
25 value: `
26 default-src 'self';
27 script-src 'self' 'unsafe-eval' 'unsafe-inline';
28 style-src 'self' 'unsafe-inline';
29 img-src 'self' data: https:;
30 `.replace(/\s{2,}/g, ' ').trim(),
31 },
32];
33
34module.exports = {
35 async headers() {
36 return [
37 {
38 source: '/(.*)',
39 headers: securityHeaders,
40 },
41 ];
42 },
43};CSP with Nonces#
1// Generate nonce per request
2import crypto from 'crypto';
3
4function generateNonce(): string {
5 return crypto.randomBytes(16).toString('base64');
6}
7
8// Express middleware
9app.use((req, res, next) => {
10 const nonce = generateNonce();
11 res.locals.nonce = nonce;
12
13 res.setHeader(
14 'Content-Security-Policy',
15 `script-src 'self' 'nonce-${nonce}'; style-src 'self' 'nonce-${nonce}';`
16 );
17
18 next();
19});
20
21// Use in template
22<script nonce="<%= nonce %>">
23 // Inline script allowed
24</script>
25
26// Next.js App Router
27// middleware.ts
28import { NextResponse } from 'next/server';
29
30export function middleware(request: Request) {
31 const nonce = Buffer.from(crypto.randomUUID()).toString('base64');
32 const cspHeader = `
33 default-src 'self';
34 script-src 'self' 'nonce-${nonce}' 'strict-dynamic';
35 style-src 'self' 'nonce-${nonce}';
36 `;
37
38 const response = NextResponse.next();
39 response.headers.set('Content-Security-Policy', cspHeader.replace(/\s{2,}/g, ' ').trim());
40 response.headers.set('x-nonce', nonce);
41
42 return response;
43}Strict-Transport-Security (HSTS)#
# Force HTTPS
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload1// Express
2app.use(
3 helmet.hsts({
4 maxAge: 31536000, // 1 year
5 includeSubDomains: true,
6 preload: true,
7 })
8);
9
10// Manual
11app.use((req, res, next) => {
12 res.setHeader(
13 'Strict-Transport-Security',
14 'max-age=31536000; includeSubDomains; preload'
15 );
16 next();
17});X-Frame-Options#
# Prevent clickjacking
X-Frame-Options: DENY
# or
X-Frame-Options: SAMEORIGIN1// Express with helmet
2app.use(helmet.frameguard({ action: 'deny' }));
3
4// Or use CSP frame-ancestors (preferred)
5// frame-ancestors 'none'; // Same as DENY
6// frame-ancestors 'self'; // Same as SAMEORIGINX-Content-Type-Options#
# Prevent MIME sniffing
X-Content-Type-Options: nosniffapp.use(helmet.noSniff());Referrer-Policy#
# Control referer header
Referrer-Policy: strict-origin-when-cross-origin1app.use(
2 helmet.referrerPolicy({
3 policy: 'strict-origin-when-cross-origin',
4 })
5);
6
7// Options:
8// no-referrer - Never send
9// no-referrer-when-downgrade - Send for HTTPS->HTTPS
10// origin - Send origin only
11// same-origin - Send for same origin only
12// strict-origin - Send origin for HTTPS only
13// strict-origin-when-cross-origin - Recommended defaultPermissions-Policy#
# Control browser features
Permissions-Policy: camera=(), microphone=(), geolocation=(self)1app.use(
2 helmet.permittedCrossDomainPolicies({
3 permittedPolicies: 'none',
4 })
5);
6
7// Manual header
8app.use((req, res, next) => {
9 res.setHeader(
10 'Permissions-Policy',
11 'camera=(), microphone=(), geolocation=(self), payment=(self)'
12 );
13 next();
14});Cross-Origin Headers#
1# Cross-Origin-Embedder-Policy
2Cross-Origin-Embedder-Policy: require-corp
3
4# Cross-Origin-Opener-Policy
5Cross-Origin-Opener-Policy: same-origin
6
7# Cross-Origin-Resource-Policy
8Cross-Origin-Resource-Policy: same-originapp.use(helmet.crossOriginEmbedderPolicy());
app.use(helmet.crossOriginOpenerPolicy());
app.use(helmet.crossOriginResourcePolicy({ policy: 'same-origin' }));Complete Helmet Configuration#
1import helmet from 'helmet';
2
3app.use(
4 helmet({
5 contentSecurityPolicy: {
6 directives: {
7 defaultSrc: ["'self'"],
8 scriptSrc: ["'self'"],
9 styleSrc: ["'self'", "'unsafe-inline'"],
10 imgSrc: ["'self'", "data:", "https:"],
11 connectSrc: ["'self'"],
12 fontSrc: ["'self'"],
13 objectSrc: ["'none'"],
14 mediaSrc: ["'self'"],
15 frameSrc: ["'none'"],
16 frameAncestors: ["'none'"],
17 baseUri: ["'self'"],
18 formAction: ["'self'"],
19 upgradeInsecureRequests: [],
20 },
21 },
22 crossOriginEmbedderPolicy: true,
23 crossOriginOpenerPolicy: { policy: 'same-origin' },
24 crossOriginResourcePolicy: { policy: 'same-origin' },
25 dnsPrefetchControl: { allow: false },
26 frameguard: { action: 'deny' },
27 hsts: {
28 maxAge: 31536000,
29 includeSubDomains: true,
30 preload: true,
31 },
32 ieNoOpen: true,
33 noSniff: true,
34 originAgentCluster: true,
35 permittedCrossDomainPolicies: { permittedPolicies: 'none' },
36 referrerPolicy: { policy: 'strict-origin-when-cross-origin' },
37 xssFilter: true,
38 })
39);Next.js Security Headers#
1// next.config.js
2const securityHeaders = [
3 {
4 key: 'X-DNS-Prefetch-Control',
5 value: 'on',
6 },
7 {
8 key: 'Strict-Transport-Security',
9 value: 'max-age=63072000; includeSubDomains; preload',
10 },
11 {
12 key: 'X-Frame-Options',
13 value: 'SAMEORIGIN',
14 },
15 {
16 key: 'X-Content-Type-Options',
17 value: 'nosniff',
18 },
19 {
20 key: 'Referrer-Policy',
21 value: 'strict-origin-when-cross-origin',
22 },
23 {
24 key: 'Permissions-Policy',
25 value: 'camera=(), microphone=(), geolocation=()',
26 },
27 {
28 key: 'Content-Security-Policy',
29 value: `
30 default-src 'self';
31 script-src 'self' 'unsafe-eval' 'unsafe-inline';
32 style-src 'self' 'unsafe-inline';
33 img-src 'self' blob: data:;
34 font-src 'self';
35 object-src 'none';
36 base-uri 'self';
37 form-action 'self';
38 frame-ancestors 'none';
39 upgrade-insecure-requests;
40 `.replace(/\s{2,}/g, ' ').trim(),
41 },
42];
43
44module.exports = {
45 async headers() {
46 return [
47 {
48 source: '/:path*',
49 headers: securityHeaders,
50 },
51 ];
52 },
53};Testing Security Headers#
1# Check headers
2curl -I https://example.com
3
4# Use online tools
5# securityheaders.com
6# observatory.mozilla.org
7
8# Local testing
9npm install -g security-headers
10security-headers https://localhost:3000Best Practices#
Implementation:
✓ Start with Report-Only mode
✓ Test thoroughly before enforcing
✓ Use helmet for Express apps
✓ Configure per environment
CSP:
✓ Start restrictive, loosen as needed
✓ Use nonces for inline scripts
✓ Avoid 'unsafe-inline' if possible
✓ Monitor CSP reports
Maintenance:
✓ Regularly review headers
✓ Test after dependencies update
✓ Monitor security reports
✓ Keep up with new standards
Conclusion#
Security headers provide defense-in-depth against common web attacks. Start with Helmet's defaults, customize CSP for your needs, and use tools like Mozilla Observatory to verify your configuration. Test thoroughly in staging before deploying to production.