HTTP Security Headers: A Complete Implementation Guide
Learn how to implement HTTP security headers — CSP, HSTS, X-Frame-Options, and CORS — with configuration examples for Nginx, Apache, and Node.js.
HTTP security headers are your first line of defense against many common web attacks. They instruct browsers to enforce security policies — blocking XSS, preventing clickjacking, controlling resource loading, and enforcing HTTPS. Yet most websites are missing critical headers or have them misconfigured.
This guide covers each important security header with practical implementation examples.
Content-Security-Policy (CSP)
CSP is the most powerful (and most complex) security header. It tells the browser exactly which resources are allowed to load and execute on your page, effectively shutting down most XSS attacks.
Basic Policy
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'
This policy says:
- default-src ‘self’ — by default, only load resources from the same origin
- script-src ‘self’ — only execute scripts from the same origin (no inline scripts, no eval)
- style-src ‘self’ ‘unsafe-inline’ — styles from same origin plus inline styles
- img-src ‘self’ data: https: — images from same origin, data URIs, and any HTTPS source
- font-src ‘self’ — fonts from same origin only
Common Directives
| Directive | Controls |
|---|---|
default-src |
Fallback for all resource types |
script-src |
JavaScript |
style-src |
CSS |
img-src |
Images |
font-src |
Fonts |
connect-src |
XHR, fetch, WebSocket connections |
frame-src |
iframes |
media-src |
Audio and video |
object-src |
Plugins (Flash, Java) |
form-action |
Form submission targets |
frame-ancestors |
Who can embed this page (replaces X-Frame-Options) |
Source Values
| Value | Meaning |
|---|---|
'self' |
Same origin |
'none' |
Block everything |
'unsafe-inline' |
Allow inline scripts/styles (weakens CSP significantly) |
'unsafe-eval' |
Allow eval() and similar |
'nonce-{random}' |
Allow specific inline scripts with matching nonce |
'strict-dynamic' |
Trust scripts loaded by already-trusted scripts |
https: |
Any HTTPS source |
data: |
Data URIs |
*.example.com |
Any subdomain of example.com |
Nonce-Based CSP (Recommended)
Instead of allowing all inline scripts with 'unsafe-inline', use nonces:
<!-- Server generates a random nonce per request -->
<script nonce="abc123def456">
// This script is allowed because the nonce matches
console.log("trusted");
</script>
Content-Security-Policy: script-src 'nonce-abc123def456' 'strict-dynamic'
'strict-dynamic' propagates trust to scripts loaded by the nonced script, so you do not need to whitelist CDN domains individually.
Report-Only Mode
Test your CSP without breaking anything:
Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-reports
Violations are reported but not blocked. Deploy in report-only mode first, analyze violations, then switch to enforcing mode.
Building a correct CSP from scratch is tedious and error-prone. Our CSP Header Generator walks you through each directive with explanations, generates the header value, and validates it for common mistakes.
Strict-Transport-Security (HSTS)
HSTS tells browsers to only connect via HTTPS, preventing SSL stripping attacks:
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
- max-age=31536000 — enforce HTTPS for one year (in seconds)
- includeSubDomains — apply to all subdomains
- preload — eligible for browser HSTS preload lists (built into the browser)
Warning: HSTS is sticky. Once set, browsers will refuse HTTP connections for max-age duration. If you lose your SSL certificate or need to serve HTTP, you cannot easily undo it. Start with a short max-age (e.g., 300 seconds) and increase gradually.
The Preload List
Adding your domain to the HSTS preload list (hstspreload.org) means browsers will enforce HTTPS for your domain even on the very first visit — before they have seen your HSTS header. This prevents attacks on the first connection but is essentially permanent. Remove your domain requires browser updates to propagate.
X-Frame-Options
Prevents your site from being embedded in iframes on other sites (clickjacking protection):
X-Frame-Options: DENY
Values:
DENY— never allow framingSAMEORIGIN— allow framing by the same originALLOW-FROM https://trusted.com— allow framing by a specific origin (deprecated, not supported by modern browsers)
CSP replacement: frame-ancestors in CSP is more flexible and replaces X-Frame-Options:
Content-Security-Policy: frame-ancestors 'self' https://trusted.com
Use both for backward compatibility with older browsers.
X-Content-Type-Options
Prevents MIME type sniffing:
X-Content-Type-Options: nosniff
Without this header, browsers may interpret a file differently from its declared Content-Type. An attacker could upload a file with a .jpg extension containing JavaScript, and the browser might execute it. nosniff forces the browser to respect the declared type.
This header has only one valid value. Always include it.
Referrer-Policy
Controls how much URL information is sent in the Referer header when navigating away from your site:
Referrer-Policy: strict-origin-when-cross-origin
Common values:
| Value | Behavior |
|---|---|
no-referrer |
Never send referrer |
origin |
Send only the origin (no path) |
strict-origin |
Send origin on HTTPS-to-HTTPS, nothing on HTTPS-to-HTTP |
strict-origin-when-cross-origin |
Full URL for same-origin, origin for cross-origin, nothing for downgrade |
unsafe-url |
Always send full URL (not recommended) |
strict-origin-when-cross-origin is the recommended default — it provides referrer information for same-origin requests (useful for analytics) while protecting path information on cross-origin requests.
Permissions-Policy
Controls which browser features your site can use:
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()
Empty parentheses () disable the feature entirely. Other options:
Permissions-Policy: camera=(self), geolocation=(self "https://maps.example.com")
This prevents embedded iframes from accessing sensitive browser APIs unless explicitly allowed.
CORS Headers
Cross-Origin Resource Sharing (CORS) headers control which origins can make requests to your API:
Access-Control-Allow-Origin: https://frontend.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Max-Age: 86400
Common CORS Mistakes
Access-Control-Allow-Origin: * with credentials: The wildcard * does not work with credentials: 'include'. You must specify the exact origin.
Reflecting the Origin header without validation: Some servers echo back whatever origin the request sends. This is effectively the same as * and defeats the purpose of CORS.
Missing preflight handling: Browsers send an OPTIONS request before certain cross-origin requests. Your server must handle OPTIONS and return the appropriate CORS headers.
For testing CORS configurations, our CORS Tester sends requests to your API and shows exactly which CORS headers are returned, helping you verify that your configuration is correct. For a deeper look at how CORS works, see our CORS explained guide.
Implementation
Nginx
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' https: data:;" always;
Apache
Header always set X-Frame-Options "SAMEORIGIN"
Header always set X-Content-Type-Options "nosniff"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
Header always set Permissions-Policy "camera=(), microphone=(), geolocation=()"
Node.js (Express)
const helmet = require('helmet');
app.use(helmet());
// Or manually:
app.use((req, res, next) => {
res.setHeader('X-Frame-Options', 'SAMEORIGIN');
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
next();
});
Checking Your Headers
After implementing security headers, verify them. Our Security Headers Checker analyzes any URL and reports which security headers are present, which are missing, and whether the values are configured correctly. It provides specific recommendations for any gaps.
Implementation Checklist
| Header | Priority | Difficulty |
|---|---|---|
| X-Content-Type-Options | High | Trivial |
| X-Frame-Options | High | Trivial |
| Strict-Transport-Security | High | Easy (if already HTTPS) |
| Referrer-Policy | Medium | Easy |
| Permissions-Policy | Medium | Easy |
| Content-Security-Policy | High | Complex |
| CORS | Situational | Medium |
Start with the easy headers — X-Content-Type-Options, X-Frame-Options, and Referrer-Policy can be added in minutes with zero risk. Then add HSTS (start with a low max-age). Finally, tackle CSP in report-only mode before enforcing.
Security headers are not a silver bullet, but they significantly raise the bar for attackers. The effort to implement them is small compared to the protection they provide.
Comments