Security Headers
HTTP security headers provide an extra layer of security by helping to detect and mitigate certain types of attacks, including XSS and data injection.
Easy Wins
Security Headers Defense Layers
Recommended Headers
Each header below addresses specific attack vectors. Implementing all of them provides comprehensive browser-level protection.
All applications should implement these headers:
# Prevent clickjacking
X-Frame-Options: DENY
# Enable browser XSS filter
X-XSS-Protection: 1; mode=block
# Prevent MIME-type sniffing
X-Content-Type-Options: nosniff
# Force HTTPS
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
# Control referrer information
Referrer-Policy: strict-origin-when-cross-origin
# Permissions Policy (formerly Feature-Policy)
Permissions-Policy: geolocation=(), microphone=(), camera=()
# Content Security Policy
Content-Security-Policy: default-src 'self'X-Frame-Options
Prevents your site from being embedded in iframes on other domains, blocking clickjacking attacks where attackers overlay invisible frames to hijack clicks.
X-Content-Type-Options
Stops browsers from guessing ("sniffing") content types. Prevents attacks where a file served as text/plain is executed as JavaScript because the browser detected script content.
Strict-Transport-Security (HSTS)
Forces browsers to only connect via HTTPS for the specified duration. Prevents SSL stripping attacks and accidental HTTP connections. Add preload for browser built-in list.
Referrer-Policy
Controls how much referrer information is sent with requests. strict-origin-when-cross-origin sends full path to same-origin, only origin to cross-origin, nothing to less secure.
Permissions-Policy
Controls which browser features (camera, microphone, geolocation) your site can use. Empty parentheses () disable the feature entirely. Limits damage if XSS occurs.
Content-Security-Policy
The most powerful security header. Defines allowed sources for scripts, styles, images, etc. default-src 'self' only allows resources from your own domain.
Implementation
Here's how to implement these headers in popular frameworks and web servers. Most provide middleware or configuration options to set headers globally.
Express.js (Node.js)
Using helmet:
const helmet = require('helmet');
app.use(helmet()); // Enables most security headers
// Or configure individually:
app.use(helmet.frameguard({ action: 'deny' }));
app.use(helmet.hsts({ maxAge: 31536000, includeSubDomains: true }));
app.use(helmet.noSniff());
app.use(helmet.xssFilter());Flask (Python)
Using flask-talisman:
from flask_talisman import Talisman
Talisman(app,
force_https=True,
strict_transport_security=True,
strict_transport_security_max_age=31536000,
content_security_policy=csp
)Django
In settings.py:
SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True
X_FRAME_OPTIONS = 'DENY'
SECURE_HSTS_SECONDS = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_SSL_REDIRECT = TrueWeb Server Config
Apache (.htaccess):
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 Strict-Transport-Security "max-age=31536000; includeSubDomains"
Header always set Referrer-Policy "strict-origin-when-cross-origin"Nginx:
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 Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;Testing Verification
Use these commands and tools to verify your security headers are properly configured.
Command Line Tests
# Check all security headers at once
curl -I https://yoursite.com | grep -iE "x-frame|x-content|strict-transport|referrer|content-security|permissions"
# Expected output should include:
# X-Frame-Options: DENY
# X-Content-Type-Options: nosniff
# Strict-Transport-Security: max-age=31536000; includeSubDomains
# Referrer-Policy: strict-origin-when-cross-origin
# Content-Security-Policy: default-src 'self'Online Testing Tools
securityheaders.com
Comprehensive header analysis with grading
observatory.mozilla.org
Mozilla's security scanner with recommendations
csp-evaluator.withgoogle.com
Google's CSP analysis tool
hstspreload.org
Check HSTS preload list eligibility
Common Mistakes
❌ Missing "always" in Nginx
# DON'T: Headers only on 2xx responses
add_header X-Frame-Options "DENY";Without "always", headers missing on error pages
✅ Correct Approach
# DO: Include "always" for all responses
add_header X-Frame-Options "DENY" always;Headers sent on all response codes
❌ HSTS Without HTTPS
# DON'T: Enable HSTS before HTTPS works
# If HTTPS breaks, users can't access site!Test HTTPS thoroughly before enabling HSTS
✅ Correct Approach
# DO: Start with short max-age, then increase
max-age=300 # 5 min (testing)
max-age=31536000 # 1 year (production)Gradually increase HSTS duration
❌ Headers Set in Wrong Location
# DON'T: Set in location block only
location /api {
add_header X-Frame-Options "DENY";
}
# Other locations unprotected!Headers not applied to all routes
✅ Correct Approach
# DO: Set in server block for all routes
server {
add_header X-Frame-Options "DENY" always;
# Applies to all locations
}Headers applied globally
Security Headers Lab
Practice analyzing and implementing HTTP security headers.
Evidence to Capture
Response classes, not one response:
The full header block for an HTML page, a JSON API response, a static asset, a redirect and a 4xx or 5xx, each with its status line. Headers commonly differ per class and per route, so a single curl -I of the homepage proves nothing.
Hostnames: Every hostname the application answers on — apex, www, api, static, the CDN origin — with the header set from each, and the Server or Via chain showing which layer owns the header.
Policy text and enforcement: The Content-Security-Policy string verbatim, plus the console violation produced by an inline script you injected in devtools. A policy that parses is not a policy that is enforced.
HSTS state:
The Strict-Transport-Security value with its max-age and whether the host is actually on the preload list, if preload is claimed.
What the gap enables: The one concrete request the missing header permits: a framed state-changing page, a script MIME-sniffed from an upload path, or a session cookie sent over HTTP.
Likely False Positives
- Header present on HTML only: If the API and error responses lack it, the deployment is inconsistent and the finding stands. Conversely, a header seen once is not evidence that it is applied everywhere.
-
Report-Only CSP:
A
Content-Security-Policy-Report-Onlyheader blocks nothing. Check the exact header name before calling the policy absent or present. - Two conflicting values from edge and app: A duplicated X-Frame-Options or a CSP overridden at the CDN is a configuration divergence. Report the effective header the browser receives, not the one in the repository.
- Scanner grade: Compliance scanners score Server disclosure and legacy headers alongside real controls, and can pass a policy containing unsafe-inline. Report header names and values, not a letter grade.
- HSTS on an internal HTTP host: A staging host serving plain HTTP cannot set HSTS meaningfully. Only prod HTTPS behaviour is the finding.
Testing Checklist
Discovery
- ☐ Enumerate the response classes the app produces and the hostnames it answers on, so nothing is sampled from one happy path.
- ☐ Note where headers are set: application middleware, framework config, reverse proxy, CDN rules. Each layer can add, drop or duplicate.
- ☐ Record the third-party script and style origins that the CSP must permit, since that constrains the fix.
Confirmation
- ☐ Request each class through the public edge, not directly to the origin, and capture the headers the browser actually receives.
- ☐ Inject an inline script and a remote script in devtools and confirm the policy blocks both, rather than only parsing.
- ☐ Check
Content-Typeandnosnifftogether on an upload path, because the pair is what stops MIME-sniffed execution.
Impact
- ☐ Frame a page that performs a state change and show the click being taken, if X-Frame-Options or frame-ancestors is the gap.
- ☐ Show a downgrade path for a cookie without Secure, or a login form reachable over HTTP, if transport is the gap.
- ☐ State which of the missing headers are hardening and which enable a demonstrated attack. Report them at different severities.
Remediation
Structural fix
- • Set the header set once, at the edge or in one middleware, for every route including error handlers and static assets, and keep the policy in version control as the single source.
- • Deploy a nonce-based CSP rather than
unsafe-inline, and pairnosniffwith an explicit Content-Type on every generated response.
Compensating control
- • Roll HSTS out with a short max-age first, confirm HTTPS on every subdomain, then raise it and submit to preload last. Enabling preload early is not reversible in practice.
- • Stage a new CSP in Report-Only with a reporting endpoint, clear the violations, then enforce. Keep the report endpoint for regressions.
- • Keep a deploy-time check that asserts the header set on a canary route, so a proxy change cannot silently remove it.
CWE References: CWE-693