How to Fix Missing Security Headers (HSTS, CSP, X-Frame-Options)
By Elizabeth Stein
Security headers are HTTP response headers that tell browsers how to behave when handling your site's content. They're your first line of defence against cross-site scripting (XSS), clickjacking, MIME sniffing attacks, and protocol downgrade exploits. Despite being straightforward to implement, most websites are missing at least half of them.
A missing Content-Security-Policy header means any injected script can run freely. A missing Strict-Transport-Security header means users can be silently downgraded from HTTPS to HTTP. These aren't theoretical risks - they're actively exploited in the wild.
The 6 Essential Security Headers
1. Strict-Transport-Security (HSTS)
What it does: Forces browsers to only connect to your site over HTTPS. After the first visit, the browser will refuse to load your site over plain HTTP for the duration you specify.
Recommended value:
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
This sets a two-year duration, covers all subdomains, and signals eligibility for the HSTS preload list - a hardcoded list in browsers that enforces HTTPS before any connection is ever made.
2. Content-Security-Policy (CSP)
What it does: Controls which resources (scripts, styles, images, fonts, frames) the browser is allowed to load on your pages. A well-configured CSP is the single most effective defence against XSS attacks.
Recommended starting value:
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'
Start strict and loosen as needed. Use Content-Security-Policy-Report-Only first to test without breaking your site - it logs violations without blocking them.
3. X-Frame-Options
What it does: Prevents your site from being loaded inside an <iframe> on another domain. This blocks clickjacking attacks where an attacker overlays your site with invisible elements to trick users into clicking.
Recommended value:
X-Frame-Options: DENY
Use SAMEORIGIN if you need to embed your own pages in iframes. Note that frame-ancestors in CSP is the modern replacement, but X-Frame-Options is still worth setting for older browser support.
4. X-Content-Type-Options
What it does: Prevents browsers from MIME-sniffing a response away from the declared Content-Type. Without this, a browser might interpret an uploaded text file as JavaScript and execute it.
Recommended value:
X-Content-Type-Options: nosniff
There's only one valid value. Always set it.
5. Referrer-Policy
What it does: Controls how much referrer information is sent when navigating away from your site. Prevents leaking sensitive URL paths (like password reset tokens or admin panel URLs) to third parties.
Recommended value:
Referrer-Policy: strict-origin-when-cross-origin
This sends the full referrer for same-origin requests but only the origin (no path) for cross-origin requests. It's the best balance between privacy and analytics functionality.
6. Permissions-Policy
What it does: Controls which browser features (camera, microphone, geolocation, payment) your site can use. Prevents third-party scripts embedded on your pages from accessing sensitive APIs without your knowledge.
Recommended value:
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()
The empty parentheses disable each feature entirely. Add your origin if you actually need a feature: camera=(self).
How to Add Security Headers
Next.js (next.config.js)
const securityHeaders = [
{ key: "Strict-Transport-Security", value: "max-age=63072000; includeSubDomains; preload" },
{ key: "Content-Security-Policy", value: "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'" },
{ 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: "camera=(), microphone=(), geolocation=(), payment=()" },
];
module.exports = {
async headers() {
return [{ source: "/(.*)", headers: securityHeaders }];
},
};
Nginx
add_header Strict-Transport-Security "max-age=63072000; 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 X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()" always;
Apache (.htaccess)
Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
Header always set 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'"
Header always set X-Frame-Options "DENY"
Header always set X-Content-Type-Options "nosniff"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()"
Vercel (vercel.json)
{
"headers": [
{
"source": "/(.*)",
"headers": [
{ "key": "Strict-Transport-Security", "value": "max-age=63072000; includeSubDomains; preload" },
{ "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": "camera=(), microphone=(), geolocation=(), payment=()" }
]
}
]
}
Verifying Your Headers
After deploying, check that your headers are actually being served. Open your browser's DevTools, go to the Network tab, click on the document request, and inspect the Response Headers.
You can also use curl -I https://yoursite.com from a terminal to quickly list all response headers.
For a comprehensive check across your entire site - not just the homepage - run a Rocket Vitals scan. The security checker validates all six headers on every crawled page and flags any that are missing or misconfigured.
Related Checks
Rocket Vitals flags each missing or misconfigured header individually. See the full details for each check:
- Missing CSP header - No Content-Security-Policy header detected
- Missing HSTS - No Strict-Transport-Security header
- Missing X-Frame-Options - Page can be embedded in frames
- Missing X-Content-Type-Options - No nosniff header set
- Missing Referrer-Policy - No referrer policy configured
- Missing Permissions-Policy - No browser feature restrictions
Run a free scan to see which security headers your site is missing and get specific recommendations for each one. Scan your site →