Security Headers
Harden web applications with HTTP security headers: CSP, HSTS, X-Frame-Options, and a thorough security header checklist.
Overview
HTTP security headers are the first line of defense for web applications. They instruct browsers on how to behave when rendering your site — blocking XSS, preventing clickjacking, enforcing HTTPS, and controlling what external resources can load. Properly configured, they can stop entire classes of attacks without changing a line of application code.
When to Use
Use this resource when:
- Hardening production web applications against common browser-based attacks
- Preparing for security audits that check OWASP secure headers
- Serving user-generated content that could contain malicious scripts
- Embedding your application in third-party sites via iframes
Solution
Express.js Security Headers Middleware
const helmet = require('helmet');
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'", "https://analytics.example.com"],
styleSrc: ["'self'", "https://fonts.googleapis.com"],
imgSrc: ["'self'", "data:", "https://cdn.example.com"],
connectSrc: ["'self'", "https://api.example.com"],
fontSrc: ["'self'", "https://fonts.gstatic.com"],
frameAncestors: ["'none'"]
}
},
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true
},
referrerPolicy: { policy: 'strict-origin-when-cross-origin' }
}));
Nginx Security Headers
server {
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline';" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
location / {
proxy_pass http://backend;
}
}
Security Header Audit Script (Bash)
#!/bin/bash
URL="https://example.com"
echo "Checking security headers for $URL..."
curl -sI "$URL" | grep -iE \
"(content-security-policy|strict-transport-security|x-content-type-options|x-frame-options|referrer-policy|permissions-policy)"
Explanation
Essential headers:
| Header | Purpose | Risk if Missing |
|---|---|---|
| Content-Security-Policy (CSP) | Controls loaded scripts/styles | XSS via injected <script> tags |
| Strict-Transport-Security (HSTS) | Forces HTTPS | SSL stripping attacks |
| X-Content-Type-Options | Prevents MIME sniffing | Drive-by downloads via fake content types |
| X-Frame-Options | Blocks clickjacking | UI redressing attacks |
| Referrer-Policy | Controls referrer leakage | Sensitive URLs exposed to third parties |
| Permissions-Policy | Restricts browser APIs | Unauthorized camera, microphone, geolocation access |
Variants
| Framework | Library | Ease |
|---|---|---|
| Express | Helmet | One line |
| Fastify | @fastify/helmet | Plugin |
| Django | django-csp | Middleware |
| Spring | Spring Security | Configuration |
| Rails | secure_headers | Gem |
| Nginx | Native | Manual |
What Works
- Start with report-only CSP:
Content-Security-Policy-Report-Onlylogs violations without blocking - Use nonce for inline scripts:
<script nonce="random-value">instead of'unsafe-inline' - Test before enforcing: CSP can break third-party widgets, payment forms, and analytics
- Submit to HSTS preload list: Include your domain in browser preload lists for automatic HTTPS
- Set appropriate frame ancestors: Use CSP
frame-ancestorsinstead of legacyX-Frame-Options
Common Mistakes
- Overly permissive CSP:
default-src *allows any domain to execute scripts - Missing on API responses: Browsers ignore headers on 404/500 pages, but attackers target them
- Forgetting API endpoints: JSON APIs should still send CORS and CSP headers
- Unreported violations: Without
report-uri, you won’t know when legitimate content is blocked - Inconsistent headers: Nginx proxy overriding application headers creates gaps
Advanced Solutions
CSP report-only with violation reporting
Deploy CSP in report-only mode first to catch violations without breaking anything:
const express = require('express');
const helmet = require('helmet');
const app = express();
// Report-only CSP: logs violations but does not block
app.use(helmet.contentSecurityPolicy({
useDefaults: false,
directives: {
'default-src': ["'self'"],
'script-src': ["'self'", 'https://cdn.example.com'],
'style-src': ["'self'", 'https://fonts.googleapis.com'],
'img-src': ["'self'", 'data:', 'https:'],
'connect-src': ["'self'", 'https://api.example.com'],
'report-uri': ['/api/csp-report'],
},
reportOnly: true,
}));
// Endpoint to receive CSP violation reports
app.post('/api/csp-report', express.json({ type: 'application/csp-report' }), (req, res) => {
const report = req.body['csp-report'];
console.warn('CSP Violation:', {
'document-uri': report['document-uri'],
'violated-directive': report['violated-directive'],
'blocked-uri': report['blocked-uri'],
'line-number': report['line-number'],
'source-file': report['source-file'],
});
res.status(204).end();
});
// After collecting reports for 1-2 weeks, switch to enforcing CSP
// by removing reportOnly: true and keeping the report-uri directive
Django security headers middleware
# settings.py
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
# ... other middleware
]
SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True
SECURE_HSTS_SECONDS = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
SECURE_SSL_REDIRECT = True
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
X_FRAME_OPTIONS = 'DENY'
# CSP via django-csp
CSP_DEFAULT_SRC = ("'self'",)
CSP_SCRIPT_SRC = ("'self'", 'https://cdn.example.com')
CSP_STYLE_SRC = ("'self'", 'https://fonts.googleapis.com')
CSP_IMG_SRC = ("'self'", 'data:', 'https:')
CSP_CONNECT_SRC = ("'self'", 'https://api.example.com')
CSP_FONT_SRC = ("'self'", 'https://fonts.gstatic.com')
CSP_FRAME_ANCESTORS = ("'none'",)
CSP_REPORT_URI = '/csp-report/'
Spring Boot security headers
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.header.writers.XXssProtectionHeaderWriter;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.headers(headers -> headers
.contentSecurityPolicy(csp -> csp.policyDirectives(
"default-src 'self'; " +
"script-src 'self' https://cdn.example.com; " +
"style-src 'self' https://fonts.googleapis.com; " +
"img-src 'self' data: https:; " +
"connect-src 'self' https://api.example.com; " +
"frame-ancestors 'none'; " +
"base-uri 'self'; " +
"object-src 'none'"
))
.httpStrictTransportSecurity(hsts -> hsts
.maxAgeInSeconds(31536000)
.includeSubDomains(true)
.preload(true)
)
.contentTypeOptions(opts -> {}) // X-Content-Type-Options: nosniff
.frameOptions(frame -> frame.deny())
.xssProtection(xss -> xss.headerValue(
XXssProtectionHeaderWriter.HeaderValue.ENABLED_MODE_BLOCK
))
.referrerPolicy(referrer -> referrer.policy(
org.springframework.security.web.header.writers.ReferrerPolicyHeaderWriter.ReferrerPolicy.STRICT_ORIGIN_WHEN_CROSS_ORIGIN
))
)
.csrf(csrf -> csrf.disable()); // Disable CSRF for API-only apps
return http.build();
}
}
Cross-Origin isolation headers (COEP, COOP, CORP)
For applications that need SharedArrayBuffer or other advanced APIs, set cross-origin isolation headers:
# Nginx: enable cross-origin isolation
server {
# Cross-Origin Embedder Policy: require CORS for cross-origin resources
add_header Cross-Origin-Embedder-Policy "require-corp" always;
# Cross-Origin Opener Policy: isolate browsing context group
add_header Cross-Origin-Opener-Policy "same-origin" always;
# Cross-Origin Resource Policy: restrict who can embed this resource
add_header Cross-Origin-Resource-Policy "same-origin" always;
# Standard security headers
add_header Content-Security-Policy "default-src 'self'" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=(), payment=(), usb=()" always;
}
Automated security header audit (Python)
import requests
from typing import dict
REQUIRED_HEADERS = {
'content-security-policy': 'Blocks XSS and resource injection',
'strict-transport-security': 'Forces HTTPS',
'x-content-type-options': 'Prevents MIME sniffing',
'x-frame-options': 'Prevents clickjacking',
'referrer-policy': 'Controls referrer leakage',
'permissions-policy': 'Restricts browser APIs',
}
def audit_security_headers(url: str) -> dict:
"""Audit a URL for missing security headers."""
try:
response = requests.head(url, allow_redirects=True, timeout=10)
except requests.RequestException as e:
return {'error': str(e)}
results = {
'url': url,
'status': response.status_code,
'present': {},
'missing': {},
'warnings': [],
}
for header, purpose in REQUIRED_HEADERS.items():
value = response.headers.get(header)
if value:
results['present'][header] = value
# Check for weak configurations
if header == 'content-security-policy' and "'unsafe-inline'" in value:
results['warnings'].append(
f"CSP contains 'unsafe-inline' — consider using nonces"
)
if header == 'strict-transport-security' and 'max-age=0' in value:
results['warnings'].append(
"HSTS max-age is 0 — HSTS is effectively disabled"
)
else:
results['missing'][header] = purpose
return results
# Usage
audit = audit_security_headers('https://example.com')
print(f"Present: {len(audit['present'])}/{len(REQUIRED_HEADERS)}")
if audit['missing']:
print("Missing headers:")
for h, p in audit['missing'].items():
print(f" - {h}: {p}")
if audit['warnings']:
print("Warnings:")
for w in audit['warnings']:
print(f" - {w}") Frequently Asked Questions
Do security headers protect APIs?
Partially. CORS, HSTS, and CSP matter for browser clients. For machine-to-machine APIs, focus on authentication and TLS.
Will CSP break my analytics?
Only if you forget to whitelist your analytics domain. Add it to script-src and connect-src directives.
What's the difference between X-Frame-Options and CSP frame-ancestors?
CSP frame-ancestors is the modern standard. X-Frame-Options is deprecated but still useful for older browsers.
Related Resources
Web Application Security (OWASP Top 10)
A developer-focused guide to the OWASP Top 10: injection, broken access control, XSS, insecure design, and how to prevent each vulnerability with code examples.
DocData Retention Policy Template
A template to define how long data is kept, when it is archived, and when it must be deleted for compliance and cost reasons.
DocThird-Party Dependency Audit Template
A template for auditing third-party dependencies: license compliance, security vulnerabilities, maintenance health, and supply chain risk.
DocPenetration Test Report Template with Remediation
Document security assessment findings with this penetration test plan template, including risk ratings, reproduction steps, and actionable remediation guidance.
DocSecurity Incident Response Template
A template for security incident response covering detection, classification, containment, eradication, recovery, communication, and post-incident review.
RecipeImplement OAuth 2.0 PKCE for Single-Page Applications
How to implement the OAuth 2.0 PKCE flow in single-page applications to securely authenticate users without exposing client secrets