Security Headers and Browser Security: Essential HTTP Headers for Web Protection
Introduction
HTTP security headers are your first line of defense against common web attacks like XSS, clickjacking, and MITM attacks. Properly configured headers can prevent vulnerabilities from being exploited, even if they exist in your code. This guide covers essential security headers with practical implementation examples.
Common Security Header Issues
- Missing Content Security Policy (CSP)
- No Strict-Transport-Security (HSTS)
- Allowing framing from any origin
- Missing X-Content-Type-Options
- Weak or missing Permissions-Policy
- No referrer control
- Allowing insecure protocols
- Missing CORS headers or misconfiguration
Content Security Policy (CSP)
Vulnerable Application (No CSP)
html<!-- VULNERABLE: No CSP protection --> <!DOCTYPE html> <html> <head> <title>Vulnerable App</title> <!-- Any inline script will execute --> <script> // Injected XSS can execute here eval(userInput); // Dangerous! </script> </head> <body> <div id="content"></div> <script src="https://cdn.example.com/library.js"></script> </body> </html>
Secure Application with CSP
javascript// Express.js - Secure CSP configuration const express = require('express'); const helmet = require('helmet'); const app = express(); // Basic CSP app.use( helmet.contentSecurityPolicy({ directives: { defaultSrc: ["'self'"], scriptSrc: [ "'self'", "'nonce-{RANDOM}'", // Use nonces for inline scripts "https://trusted-cdn.example.com" ], styleSrc: [ "'self'", "'nonce-{RANDOM}'", "https://trusted-cdn.example.com" ], imgSrc: ["'self'", "data:", "https:"], fontSrc: ["'self'", "https://fonts.gstatic.com"], connectSrc: ["'self'", "https://api.example.com"], frameSrc: ["'none'"], objectSrc: ["'none'"], baseUri: ["'self'"], formAction: ["'self'"], frameAncestors: ["'none'"], upgradeInsecureRequests: [], blockAllMixedContent: [] }, reportOnly: false // Set to true for testing }) );
CSP with Nonces
javascript// Generate unique nonce per request const crypto = require('crypto'); app.use((req, res, next) => { res.locals.nonce = crypto.randomBytes(16).toString('base64'); next(); }); app.use( helmet.contentSecurityPolicy({ directives: { scriptSrc: [ "'self'", (req, res) => `'nonce-${res.locals.nonce}'` ], styleSrc: [ "'self'", (req, res) => `'nonce-${res.locals.nonce}'` ] } }) ); // In your template (EJS example) // <script nonce="<%= nonce %>"> // console.log('This script is allowed'); // </script>
CSP Reporting
javascriptapp.use( helmet.contentSecurityPolicy({ directives: { defaultSrc: ["'self'"], scriptSrc: ["'self'"], reportUri: "/csp-violation-report", // Deprecated reportTo: "csp-endpoint" // New standard } }) ); // Add Report-To header app.use((req, res, next) => { res.setHeader('Report-To', JSON.stringify({ group: 'csp-endpoint', max_age: 10886400, endpoints: [{ url: 'https://example.com/csp-reports' }] })); next(); }); // CSP violation report endpoint app.post('/csp-violation-report', express.json({ type: ['application/json', 'application/csp-report'] }), (req, res) => { console.log('CSP Violation:', req.body); // Log to your monitoring system res.status(204).end(); });
Strict-Transport-Security (HSTS)
javascript// HSTS Configuration app.use( helmet.hsts({ maxAge: 31536000, // 1 year in seconds includeSubDomains: true, preload: true }) ); // Output: Strict-Transport-Security: max-age=31536000; includeSubDomains; preload // For HSTS preload list submission, visit: https://hstspreload.org/
Nginx HSTS Configuration
nginxserver { listen 443 ssl http2; server_name example.com; # HSTS add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always; # Redirect HTTP to HTTPS error_page 497 https://$host$request_uri; } server { listen 80; server_name example.com; # Redirect all HTTP to HTTPS return 301 https://$server_name$request_uri; }
X-Frame-Options and Frame-Ancestors
javascript// Prevent clickjacking app.use(helmet.frameguard({ action: 'deny' })); // Output: X-Frame-Options: DENY // Or allow from same origin app.use(helmet.frameguard({ action: 'sameorigin' })); // Output: X-Frame-Options: SAMEORIGIN // CSP frame-ancestors is more flexible (recommended) app.use( helmet.contentSecurityPolicy({ directives: { frameAncestors: ["'self'", "https://trusted.example.com"] } }) );
X-Content-Type-Options
javascript// Prevent MIME sniffing app.use(helmet.noSniff()); // Output: X-Content-Type-Options: nosniff // Nginx // add_header X-Content-Type-Options "nosniff" always;
Referrer-Policy
javascript// Control referrer information app.use( helmet.referrerPolicy({ policy: 'strict-origin-when-cross-origin' }) ); // Available policies: // - no-referrer: Never send referrer // - no-referrer-when-downgrade: Default behavior // - origin: Send only origin // - origin-when-cross-origin: Full URL for same-origin, origin for cross-origin // - same-origin: Only for same-origin requests // - strict-origin: Origin only, no HTTPS->HTTP // - strict-origin-when-cross-origin: Full URL same-origin, origin cross-origin (recommended) // - unsafe-url: Always send full URL (not recommended)
Permissions-Policy (formerly Feature-Policy)
javascript// Control browser features app.use((req, res, next) => { res.setHeader('Permissions-Policy', 'camera=(), ' + 'microphone=(), ' + 'geolocation=(self), ' + 'payment=(self "https://payment.example.com"), ' + 'usb=(), ' + 'accelerometer=(), ' + 'gyroscope=(), ' + 'magnetometer=()' ); next(); });
Nginx Permissions-Policy
nginxadd_header Permissions-Policy "camera=(), microphone=(), geolocation=(self), payment=(self)" always;
X-XSS-Protection
javascript// Enable XSS filtering (legacy, CSP is better) app.use(helmet.xssFilter()); // Output: X-XSS-Protection: 1; mode=block // Note: This header is deprecated. Use CSP instead. // Some modern browsers ignore this header.
Cross-Origin Headers
CORP, COEP, COOP
javascript// Cross-Origin-Resource-Policy app.use((req, res, next) => { res.setHeader('Cross-Origin-Resource-Policy', 'same-origin'); // or 'same-site' or 'cross-origin' next(); }); // Cross-Origin-Embedder-Policy app.use((req, res, next) => { res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp'); next(); }); // Cross-Origin-Opener-Policy app.use((req, res, next) => { res.setHeader('Cross-Origin-Opener-Policy', 'same-origin'); next(); }); // Using Helmet app.use(helmet.crossOriginResourcePolicy({ policy: 'same-origin' })); app.use(helmet.crossOriginEmbedderPolicy()); app.use(helmet.crossOriginOpenerPolicy({ policy: 'same-origin' }));
Complete Secure Headers Configuration
Express.js with Helmet
javascriptconst express = require('express'); const helmet = require('helmet'); const crypto = require('crypto'); const app = express(); // Generate nonce for CSP app.use((req, res, next) => { res.locals.nonce = crypto.randomBytes(16).toString('base64'); next(); }); // Comprehensive Helmet configuration app.use( helmet({ // Content Security Policy contentSecurityPolicy: { directives: { defaultSrc: ["'self'"], scriptSrc: [ "'self'", (req, res) => `'nonce-${res.locals.nonce}'`, "https://trusted-cdn.com" ], styleSrc: [ "'self'", (req, res) => `'nonce-${res.locals.nonce}'`, "https://trusted-cdn.com" ], imgSrc: ["'self'", "data:", "https:"], fontSrc: ["'self'", "https://fonts.gstatic.com"], connectSrc: ["'self'", "https://api.example.com"], frameSrc: ["'none'"], objectSrc: ["'none'"], baseUri: ["'self'"], formAction: ["'self'"], frameAncestors: ["'none'"], upgradeInsecureRequests: [], reportTo: "csp-endpoint" } }, // HSTS hsts: { maxAge: 31536000, includeSubDomains: true, preload: true }, // X-Frame-Options frameguard: { action: 'deny' }, // Referrer Policy referrerPolicy: { policy: 'strict-origin-when-cross-origin' }, // Cross-Origin policies crossOriginEmbedderPolicy: true, crossOriginOpenerPolicy: { policy: 'same-origin' }, crossOriginResourcePolicy: { policy: 'same-origin' }, // Other headers noSniff: true, xssFilter: true, dnsPrefetchControl: { allow: false }, ieNoOpen: true, hidePoweredBy: true }) ); // Additional custom headers app.use((req, res, next) => { // Permissions Policy res.setHeader('Permissions-Policy', 'camera=(), microphone=(), geolocation=(self), payment=()' ); // Report-To res.setHeader('Report-To', JSON.stringify({ group: 'csp-endpoint', max_age: 10886400, endpoints: [{ url: 'https://example.com/reports' }] })); // Expect-CT (deprecated but some use it) res.setHeader('Expect-CT', 'max-age=86400, enforce, report-uri="https://example.com/ct-reports"' ); next(); }); app.listen(3000);
Nginx Complete Configuration
nginxserver { listen 443 ssl http2; server_name example.com; # SSL Configuration ssl_certificate /path/to/cert.pem; ssl_certificate_key /path/to/key.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; ssl_prefer_server_ciphers on; # Security Headers add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always; add_header X-Frame-Options "DENY" always; add_header X-Content-Type-Options "nosniff" always; add_header X-XSS-Protection "1; mode=block" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; add_header Permissions-Policy "camera=(), microphone=(), geolocation=(self)" always; # CSP add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'nonce-random'; style-src 'self' 'nonce-random'; img-src 'self' data: https:; font-src 'self'; connect-src 'self'; frame-src 'none'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; upgrade-insecure-requests; block-all-mixed-content" always; # Cross-Origin policies add_header Cross-Origin-Embedder-Policy "require-corp" always; add_header Cross-Origin-Opener-Policy "same-origin" always; add_header Cross-Origin-Resource-Policy "same-origin" always; # Remove sensitive headers proxy_hide_header X-Powered-By; proxy_hide_header Server; location / { proxy_pass http://backend; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } }
Apache Complete Configuration
apache<VirtualHost *:443> ServerName example.com # SSL Configuration SSLEngine on SSLCertificateFile /path/to/cert.pem SSLCertificateKeyFile /path/to/key.pem SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1 SSLCipherSuite HIGH:!aNULL:!MD5 # Security Headers Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" Header always set X-Frame-Options "DENY" Header always set X-Content-Type-Options "nosniff" Header always set X-XSS-Protection "1; mode=block" Header always set Referrer-Policy "strict-origin-when-cross-origin" Header always set Permissions-Policy "camera=(), microphone=(), geolocation=(self)" # CSP Header always set Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data: https:; font-src 'self'; connect-src 'self'; frame-src 'none'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'" # Cross-Origin policies Header always set Cross-Origin-Embedder-Policy "require-corp" Header always set Cross-Origin-Opener-Policy "same-origin" Header always set Cross-Origin-Resource-Policy "same-origin" # Remove sensitive headers Header unset X-Powered-By Header unset Server ServerTokens Prod </VirtualHost>
Testing Security Headers
bash# Test with curl curl -I https://example.com # Test specific header curl -I https://example.com | grep -i strict-transport-security # Use securityheaders.com curl https://securityheaders.com/?q=example.com&followRedirects=on # Use Mozilla Observatory curl https://observatory.mozilla.org/analyze/example.com
Automated Testing
javascript// Jest test for security headers const request = require('supertest'); const app = require('../app'); describe('Security Headers', () => { it('should set HSTS header', async () => { const response = await request(app).get('/'); expect(response.headers['strict-transport-security']).toBeDefined(); expect(response.headers['strict-transport-security']).toContain('max-age=31536000'); }); it('should set X-Frame-Options', async () => { const response = await request(app).get('/'); expect(response.headers['x-frame-options']).toBe('DENY'); }); it('should set CSP header', async () => { const response = await request(app).get('/'); expect(response.headers['content-security-policy']).toBeDefined(); expect(response.headers['content-security-policy']).toContain("default-src 'self'"); }); it('should set X-Content-Type-Options', async () => { const response = await request(app).get('/'); expect(response.headers['x-content-type-options']).toBe('nosniff'); }); });
Security Headers Checklist
- ✅ Implement strict Content Security Policy
- ✅ Enable HSTS with preload
- ✅ Set X-Frame-Options or frame-ancestors
- ✅ Enable X-Content-Type-Options: nosniff
- ✅ Configure Referrer-Policy
- ✅ Set Permissions-Policy for features
- ✅ Implement Cross-Origin policies (CORP, COEP, COOP)
- ✅ Remove X-Powered-By and Server headers
- ✅ Use CSP nonces for inline scripts/styles
- ✅ Enable CSP reporting
- ✅ Upgrade insecure requests
- ✅ Block mixed content
- ✅ Test headers regularly
- ✅ Monitor CSP violations
- ✅ Keep headers up to date with standards
Conclusion
HTTP security headers provide essential protection against common web attacks. By implementing CSP, HSTS, and other security headers, you add critical defense layers that protect users even when vulnerabilities exist in your application code.
Security headers should be regularly reviewed and updated as browser capabilities and security standards evolve. For comprehensive web security assessments and header configuration reviews, contact the Whitespots team for expert consultation.


