Security headers are your first line of defense against common web attacks. They're easy to implement and significantly improve your application's security posture.
Essential Security Headers#
Content-Security-Policy (CSP)#
1// Express middleware
2app.use((req, res, next) => {
3 res.setHeader('Content-Security-Policy', [
4 "default-src 'self'",
5 "script-src 'self' 'unsafe-inline' https://cdn.example.com",
6 "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
7 "img-src 'self' data: https:",
8 "font-src 'self' https://fonts.gstatic.com",
9 "connect-src 'self' https://api.example.com",
10 "frame-ancestors 'none'",
11 "base-uri 'self'",
12 "form-action 'self'",
13 ].join('; '));
14 next();
15});CSP Directives Explained#
Directive Purpose
─────────────────────────────────────────────────────
default-src Fallback for other directives
script-src JavaScript sources
style-src CSS sources
img-src Image sources
font-src Font file sources
connect-src XHR, WebSocket, fetch destinations
frame-src iframe sources
frame-ancestors Who can embed this page
base-uri Restrict <base> element
form-action Form submission destinations
upgrade-insecure Upgrade HTTP to HTTPS
Strict CSP with Nonces#
1import crypto from 'crypto';
2
3app.use((req, res, next) => {
4 // Generate unique nonce per request
5 const nonce = crypto.randomBytes(16).toString('base64');
6 res.locals.nonce = nonce;
7
8 res.setHeader('Content-Security-Policy', [
9 "default-src 'self'",
10 `script-src 'self' 'nonce-${nonce}'`,
11 `style-src 'self' 'nonce-${nonce}'`,
12 ].join('; '));
13
14 next();
15});
16
17// In templates
18// <script nonce="<%= nonce %>">...</script>X-Content-Type-Options#
// Prevent MIME type sniffing
res.setHeader('X-Content-Type-Options', 'nosniff');X-Frame-Options#
// Prevent clickjacking
res.setHeader('X-Frame-Options', 'DENY');
// Or allow same origin
res.setHeader('X-Frame-Options', 'SAMEORIGIN');Referrer-Policy#
1// Control referrer information
2res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
3
4// Options:
5// no-referrer - Never send
6// no-referrer-when-downgrade - Don't send on HTTPS→HTTP
7// same-origin - Only same origin
8// strict-origin - Send origin, not path
9// strict-origin-when-cross-origin - Full URL same origin, origin cross-originPermissions-Policy#
1// Control browser features
2res.setHeader('Permissions-Policy', [
3 'camera=()', // Disable camera
4 'microphone=()', // Disable microphone
5 'geolocation=(self)', // Only self can use geolocation
6 'payment=(self)', // Only self can use payment
7].join(', '));HTTPS Configuration#
HSTS (HTTP Strict Transport Security)#
1// Force HTTPS
2res.setHeader(
3 'Strict-Transport-Security',
4 'max-age=31536000; includeSubDomains; preload'
5);
6
7// max-age: How long to remember (1 year)
8// includeSubDomains: Apply to all subdomains
9// preload: Submit to browser preload listRedirect HTTP to HTTPS#
1// Express middleware
2app.use((req, res, next) => {
3 if (req.secure || req.headers['x-forwarded-proto'] === 'https') {
4 return next();
5 }
6 res.redirect(301, `https://${req.headers.host}${req.url}`);
7});
8
9// Nginx
10server {
11 listen 80;
12 server_name example.com;
13 return 301 https://$server_name$request_uri;
14}TLS Configuration#
1# Modern TLS configuration (nginx)
2server {
3 listen 443 ssl http2;
4
5 ssl_certificate /path/to/cert.pem;
6 ssl_certificate_key /path/to/key.pem;
7
8 # Modern protocols only
9 ssl_protocols TLSv1.2 TLSv1.3;
10
11 # Strong ciphers
12 ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
13 ssl_prefer_server_ciphers off;
14
15 # OCSP Stapling
16 ssl_stapling on;
17 ssl_stapling_verify on;
18
19 # Session configuration
20 ssl_session_timeout 1d;
21 ssl_session_cache shared:SSL:10m;
22 ssl_session_tickets off;
23}Secure Cookies#
1// Secure cookie configuration
2app.use(session({
3 name: 'sessionId',
4 secret: process.env.SESSION_SECRET,
5 cookie: {
6 secure: true, // HTTPS only
7 httpOnly: true, // No JavaScript access
8 sameSite: 'strict', // CSRF protection
9 maxAge: 3600000, // 1 hour
10 domain: '.example.com',
11 path: '/',
12 },
13 resave: false,
14 saveUninitialized: false,
15}));
16
17// Set-Cookie header result:
18// Set-Cookie: sessionId=abc123; Secure; HttpOnly; SameSite=Strict; Path=/Cookie Attributes#
Attribute Purpose
────────────────────────────────────────────────────
Secure Only sent over HTTPS
HttpOnly Not accessible via JavaScript
SameSite CSRF protection
- Strict Only same-site requests
- Lax GET from external sites OK
- None Cross-site (requires Secure)
Domain Which domains receive cookie
Path URL path scope
Max-Age Lifetime in seconds
Expires Absolute expiration date
Using Helmet.js#
1import helmet from 'helmet';
2
3// Apply all security headers
4app.use(helmet());
5
6// Or configure individually
7app.use(helmet({
8 contentSecurityPolicy: {
9 directives: {
10 defaultSrc: ["'self'"],
11 scriptSrc: ["'self'", "https://cdn.example.com"],
12 styleSrc: ["'self'", "'unsafe-inline'"],
13 imgSrc: ["'self'", "data:", "https:"],
14 },
15 },
16 hsts: {
17 maxAge: 31536000,
18 includeSubDomains: true,
19 preload: true,
20 },
21 referrerPolicy: {
22 policy: 'strict-origin-when-cross-origin',
23 },
24}));Next.js Configuration#
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=31536000; includeSubDomains',
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: 'Content-Security-Policy',
25 value: "default-src 'self'; script-src 'self' 'unsafe-eval' 'unsafe-inline'",
26 },
27];
28
29module.exports = {
30 async headers() {
31 return [
32 {
33 source: '/:path*',
34 headers: securityHeaders,
35 },
36 ];
37 },
38};Testing Security Headers#
Online Tools#
- securityheaders.com - Grade your headers
- observatory.mozilla.org - Mozilla security scanner
- csp-evaluator.withgoogle.com - CSP analysis
Command Line#
1# Check headers
2curl -I https://example.com
3
4# Detailed SSL check
5openssl s_client -connect example.com:443 -servername example.com
6
7# SSL Labs test
8npx ssllabs-scan example.comAutomated Testing#
1// Jest test for security headers
2describe('Security Headers', () => {
3 it('includes all required security headers', async () => {
4 const response = await fetch('https://example.com');
5 const headers = response.headers;
6
7 expect(headers.get('Strict-Transport-Security')).toContain('max-age=');
8 expect(headers.get('X-Content-Type-Options')).toBe('nosniff');
9 expect(headers.get('X-Frame-Options')).toBe('DENY');
10 expect(headers.get('Content-Security-Policy')).toBeDefined();
11 expect(headers.get('Referrer-Policy')).toBeDefined();
12 });
13});CSP Reporting#
1// Collect CSP violations
2app.post('/csp-report', express.json({ type: 'application/csp-report' }), (req, res) => {
3 console.log('CSP Violation:', req.body);
4 // Send to logging service
5 res.status(204).end();
6});
7
8// Enable reporting in CSP
9res.setHeader('Content-Security-Policy', [
10 "default-src 'self'",
11 "report-uri /csp-report",
12 "report-to csp-endpoint",
13].join('; '));
14
15res.setHeader('Report-To', JSON.stringify({
16 group: 'csp-endpoint',
17 max_age: 86400,
18 endpoints: [{ url: '/csp-report' }],
19}));Conclusion#
Security headers are low-effort, high-impact protections. Start with the essentials (HSTS, CSP, X-Content-Type-Options), then add more as needed. Use tools like Helmet.js to simplify implementation.
Test your headers regularly, monitor CSP reports, and keep your TLS configuration updated. Security is an ongoing process, not a one-time setup.