Security Headers and Browser Security: Essential HTTP Headers for Web Protection

Security Headers and Browser Security: Essential HTTP Headers for Web Protection

Whitespots Team ·
web
headers
browser
csp

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

  1. Missing Content Security Policy (CSP)
  2. No Strict-Transport-Security (HSTS)
  3. Allowing framing from any origin
  4. Missing X-Content-Type-Options
  5. Weak or missing Permissions-Policy
  6. No referrer control
  7. Allowing insecure protocols
  8. 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

javascript
app.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

nginx
server { 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

nginx
add_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

javascript
const 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

nginx
server { 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.

Related