Security Headers: Complete Configuration Guide
Security headers are HTTP response headers that instruct browsers how to behave when handling your site's content. They form a critical security layer that costs nothing to implement but prevents...
Key Insights
- Security headers are your first line of defense against XSS, clickjacking, and data injection attacks—yet most applications ship with incomplete or misconfigured headers.
- Content-Security-Policy is the most powerful security header but also the most complex; start with report-only mode and progressively tighten restrictions.
- Implement headers at the infrastructure level (Nginx, CDN) rather than application code whenever possible for consistent, performant protection.
Introduction to Security Headers
Security headers are HTTP response headers that instruct browsers how to behave when handling your site’s content. They form a critical security layer that costs nothing to implement but prevents entire categories of attacks.
When a browser receives your response, it checks these headers before rendering content. A properly configured Content-Security-Policy stops XSS attacks cold. X-Frame-Options prevents your site from being embedded in malicious iframes. Strict-Transport-Security ensures all communication happens over HTTPS.
The browser becomes your security enforcement mechanism. Attackers can’t bypass these protections because they’re enforced client-side before malicious code executes. This is defense in depth—even if an attacker finds an injection vulnerability, security headers can prevent exploitation.
Content-Security-Policy (CSP)
CSP is the most powerful and complex security header. It defines exactly what content sources the browser should trust, blocking everything else.
Understanding Directives
CSP uses directives to control different resource types:
default-src: Fallback for all resource typesscript-src: JavaScript sourcesstyle-src: CSS sourcesimg-src: Image sourcesconnect-src: XHR, WebSocket, and fetch destinationsframe-src: Iframe sources
Here’s a basic CSP that blocks most attacks:
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'none';
This policy only allows resources from your own origin. It’s restrictive but breaks most real-world applications that use CDNs or inline scripts.
Progressive CSP Implementation
Start with report-only mode to understand what your application actually loads:
Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-report;
Once you’ve identified legitimate sources, build a production policy:
Content-Security-Policy:
default-src 'self';
script-src 'self' https://cdn.example.com 'nonce-abc123';
style-src 'self' 'unsafe-inline';
img-src 'self' https: data:;
font-src 'self' https://fonts.gstatic.com;
connect-src 'self' https://api.example.com;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
report-uri /csp-report;
Nonce-Based Inline Scripts
Avoid 'unsafe-inline' for scripts. Use nonces instead:
<script nonce="abc123def456">
// This script executes because the nonce matches
initializeApp();
</script>
Generate a cryptographically random nonce per request:
const crypto = require('crypto');
function generateNonce() {
return crypto.randomBytes(16).toString('base64');
}
app.use((req, res, next) => {
res.locals.nonce = generateNonce();
res.setHeader(
'Content-Security-Policy',
`script-src 'self' 'nonce-${res.locals.nonce}'`
);
next();
});
Cross-Origin Headers
Cross-origin policies control how your resources interact with other origins.
CORS Configuration
CORS headers control which origins can access your API:
// Express.js CORS configuration
const corsOptions = {
origin: ['https://app.example.com', 'https://admin.example.com'],
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true,
maxAge: 86400 // Cache preflight for 24 hours
};
app.use(cors(corsOptions));
For Nginx, configure CORS at the server level:
location /api/ {
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' 'https://app.example.com';
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE';
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization';
add_header 'Access-Control-Max-Age' 86400;
return 204;
}
add_header 'Access-Control-Allow-Origin' 'https://app.example.com';
add_header 'Access-Control-Allow-Credentials' 'true';
}
COOP and COEP
Cross-Origin-Opener-Policy (COOP) and Cross-Origin-Embedder-Policy (COEP) enable powerful features like SharedArrayBuffer:
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
These headers isolate your browsing context, preventing Spectre-style attacks. Required for high-resolution timers and shared memory.
Transport Security Headers
Transport headers ensure secure communication between browser and server.
HSTS Configuration
Strict-Transport-Security forces HTTPS connections:
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
max-age=31536000: Remember for one yearincludeSubDomains: Apply to all subdomainspreload: Submit to browser preload lists
Before enabling preload, ensure every subdomain supports HTTPS. Once preloaded, you cannot easily revert.
MIME Sniffing Prevention
Prevent browsers from guessing content types:
X-Content-Type-Options: nosniff
Always pair this with correct Content-Type headers:
app.use((req, res, next) => {
res.setHeader('X-Content-Type-Options', 'nosniff');
next();
});
// Serve JSON with correct type
app.get('/api/data', (req, res) => {
res.type('application/json').json({ data: 'value' });
});
Frame and Embedding Protection
Prevent your site from being embedded in malicious contexts.
Clickjacking Prevention
Use CSP’s frame-ancestors directive (preferred) or X-Frame-Options:
Content-Security-Policy: frame-ancestors 'none';
X-Frame-Options: DENY
To allow specific embedders:
Content-Security-Policy: frame-ancestors 'self' https://trusted-partner.com;
X-Frame-Options: ALLOW-FROM https://trusted-partner.com
Note: X-Frame-Options: ALLOW-FROM has limited browser support. Prefer frame-ancestors.
Permissions-Policy
Restrict browser features your application doesn’t need:
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=(), usb=(), magnetometer=(), gyroscope=(), accelerometer=()
This policy disables all listed features. To allow specific origins:
Permissions-Policy: geolocation=(self "https://maps.example.com"), camera=()
Implementation Across Platforms
Nginx Configuration
server {
# Security headers
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self'; frame-ancestors 'none';" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
}
Apache Configuration
<IfModule mod_headers.c>
Header always set X-Content-Type-Options "nosniff"
Header always set X-Frame-Options "DENY"
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
Header always set Content-Security-Policy "default-src 'self'; script-src 'self'; frame-ancestors 'none';"
Header always set Permissions-Policy "camera=(), microphone=(), geolocation=()"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
</IfModule>
Express.js Middleware
Use Helmet for comprehensive header management:
const helmet = require('helmet');
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", (req, res) => `'nonce-${res.locals.nonce}'`],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https:"],
connectSrc: ["'self'"],
frameAncestors: ["'none'"],
},
},
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true,
},
frameguard: { action: 'deny' },
referrerPolicy: { policy: 'strict-origin-when-cross-origin' },
}));
Python/Flask Middleware
from flask import Flask
from flask_talisman import Talisman
app = Flask(__name__)
csp = {
'default-src': "'self'",
'script-src': "'self'",
'style-src': "'self' 'unsafe-inline'",
'img-src': "'self' data: https:",
'frame-ancestors': "'none'",
}
Talisman(
app,
content_security_policy=csp,
strict_transport_security=True,
strict_transport_security_max_age=31536000,
strict_transport_security_include_subdomains=True,
)
Testing and Monitoring
Auditing Tools
Check your headers at securityheaders.com or use browser DevTools. Automate testing in CI:
// header-test.js
const https = require('https');
const requiredHeaders = {
'strict-transport-security': /max-age=\d+/,
'x-content-type-options': 'nosniff',
'x-frame-options': /DENY|SAMEORIGIN/,
'content-security-policy': /default-src/,
};
https.get('https://example.com', (res) => {
let failures = [];
for (const [header, expected] of Object.entries(requiredHeaders)) {
const value = res.headers[header];
if (!value) {
failures.push(`Missing: ${header}`);
} else if (expected instanceof RegExp && !expected.test(value)) {
failures.push(`Invalid ${header}: ${value}`);
} else if (typeof expected === 'string' && value !== expected) {
failures.push(`Invalid ${header}: ${value}`);
}
}
if (failures.length) {
console.error('Header validation failed:', failures);
process.exit(1);
}
console.log('All security headers validated');
});
CSP Violation Reporting
Implement a report handler to catch violations:
app.post('/csp-report', express.json({ type: 'application/csp-report' }), (req, res) => {
const report = req.body['csp-report'];
console.log('CSP Violation:', {
blockedUri: report['blocked-uri'],
violatedDirective: report['violated-directive'],
documentUri: report['document-uri'],
sourceFile: report['source-file'],
lineNumber: report['line-number'],
});
// Send to monitoring service
metrics.increment('csp.violation', {
directive: report['violated-directive'],
});
res.status(204).end();
});
Common Misconfigurations
Avoid these mistakes:
- Overly permissive CSP:
script-src 'unsafe-inline' 'unsafe-eval'defeats the purpose - Missing
alwaysin Nginx: Headers won’t be sent on error responses - HSTS without testing: Enabling preload before all subdomains support HTTPS
- Wildcard CORS:
Access-Control-Allow-Origin: *with credentials fails silently
Security headers require ongoing maintenance. Review them quarterly, monitor CSP reports, and test after every deployment. The investment pays dividends—these headers stop attacks that would otherwise bypass your application security.