Back to Blog
SecurityHTTP HeadersWeb SecurityBest Practices

Essential Web Security Headers

Protect your web app with security headers. From CSP to HSTS to X-Frame-Options and more.

B
Bootspring Team
Engineering
July 27, 2021
5 min read

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; preload
1// 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: SAMEORIGIN
1// 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 SAMEORIGIN

X-Content-Type-Options#

# Prevent MIME sniffing X-Content-Type-Options: nosniff
app.use(helmet.noSniff());

Referrer-Policy#

# Control referer header Referrer-Policy: strict-origin-when-cross-origin
1app.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 default

Permissions-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-origin
app.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:3000

Best 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.

Share this article

Help spread the word about Bootspring