Most developers think about SQL injection, XSS, and authentication. Far fewer think about HTTP response headers — the silent layer that tells browsers how to behave when they load your site. A misconfigured or absent security header won't break your application. It will just leave the door open for clickjacking, cross-site scripting, sniffing attacks, and credential theft.
The good news: checking your headers takes 30 seconds. Fixing the most important ones takes minutes. This guide walks through each header that matters, how to test whether it's present, and what to do when it's missing.
HTTP security headers are directives sent in the HTTP response from your server to the browser. They instruct the browser about what content to trust, which origins can load your resources, and how to handle protocol upgrades and permissions.
They don't protect your backend. They protect your users — specifically, they constrain what an attacker can do if they manage to inject malicious content into your pages or intercept traffic between your server and your users.
Six headers do the heavy lifting:
http://.<frame> or <iframe> on another origin, blocking clickjacking.The fastest way to inspect what your server is sending is a single curl command:
curl -I https://yourdomain.com
The -I flag fetches only the HTTP headers. You'll see output like:
HTTP/2 200
content-type: text/html; charset=utf-8
strict-transport-security: max-age=31536000; includeSubDomains
x-content-type-options: nosniff
x-frame-options: SAMEORIGIN
referrer-policy: strict-origin-when-cross-origin
Scan the list. Any security header that doesn't appear is missing. To filter specifically for the six headers above:
curl -sI https://yourdomain.com | grep -iE \
"content-security-policy|strict-transport-security|x-frame-options|x-content-type-options|referrer-policy|permissions-policy"
If a header produces no output line, it's absent from your responses.
| Missing header | Risk | Severity |
|---|---|---|
Content-Security-Policy |
An attacker who injects inline script (e.g., via XSS) can execute arbitrary JavaScript in your users' browsers — stealing sessions, credentials, and data. | High |
Strict-Transport-Security |
Users accessing http:// can be intercepted before the redirect to HTTPS fires. Also leaves subdomains vulnerable to downgrade attacks if includeSubDomains is absent. |
High |
X-Frame-Options |
Your page can be loaded inside an invisible iframe on a malicious site. A user clicking what appears to be a button on the attacker's site is actually clicking a button on yours — submitting forms, granting permissions, making purchases. | Medium–High |
X-Content-Type-Options |
Older browsers may interpret a response as executable even when your server declares it as plain text. A user-uploaded file served from your domain could run as JavaScript. | Medium |
Referrer-Policy |
Full URLs — including path, query parameters, and tokens — are sent as the Referer header to external sites your pages link to. Password reset tokens and session identifiers embedded in URLs leak to third parties. |
Medium |
Permissions-Policy |
Embedded iframes and injected scripts can silently access camera, microphone, geolocation, and payment APIs without user re-consent, depending on browser defaults. | Low–Medium |
CSP is the most complex header to configure correctly, but you don't need a perfect policy immediately. Start with a report-only policy to observe what would be blocked, then tighten it:
# Nginx
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; frame-ancestors 'none';" always;
# Apache
Header always set Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; frame-ancestors 'none';"
The frame-ancestors 'none' directive in CSP supersedes X-Frame-Options in modern browsers — set both for backward compatibility.
# Nginx
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
# Apache
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
Start with max-age=300 (5 minutes) when first deploying. Once you've confirmed HTTPS works correctly across all subdomains, raise it to 31536000 (1 year) and consider HSTS preload submission.
# Nginx
add_header X-Frame-Options "SAMEORIGIN" always;
# Apache
Header always set X-Frame-Options "SAMEORIGIN"
Use DENY if your site should never be framed at all. Use SAMEORIGIN if you embed your own pages in iframes on the same origin.
This one has no configuration — it's always the same value:
# Nginx
add_header X-Content-Type-Options "nosniff" always;
# Apache
Header always set X-Content-Type-Options "nosniff"
# Nginx
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# Apache
Header always set Referrer-Policy "strict-origin-when-cross-origin"
strict-origin-when-cross-origin is the current browser default and a safe choice for most sites. It sends the full URL for same-origin requests and only the origin (no path or query) for cross-origin requests.
# Nginx
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()" always;
# Apache
Header always set Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()"
An empty parentheses value (()) disables the feature entirely. List only the APIs you actually use as self and deny everything else.
always flag in Nginx
Without always, Nginx only sends add_header directives on 2xx and 3xx responses. Error pages (400, 404, 500) — which are frequently targeted by injection attempts — will miss the headers entirely.
After deploying a header change, confirm it's live with curl from outside your network:
curl -sI https://yourdomain.com | grep -i "content-security-policy"
If you're behind a CDN, purge the cache first — stale cached responses won't carry new headers. For Cloudflare, the Development Mode toggle or a Cache Purge from the dashboard forces fresh responses immediately.
Also verify your error pages carry the same headers:
curl -sI https://yourdomain.com/404-page-that-does-not-exist | grep -iE "x-frame-options|x-content-type"
Infrastructure changes — CDN config edits, reverse proxy updates, framework upgrades, new deploy pipelines — silently modify headers. A header that was correctly set last quarter may have been stripped or weakened by a configuration drift you never noticed.
Security headers aren't a one-time checkbox. They need ongoing monitoring the same way SSL certificates do.
Instantly see which security headers your site is sending and which are missing — no signup, no install. Paste your URL and get a full report in seconds.
Check your headers now →If you want to know immediately when a header gets dropped or weakened — without running curl on a schedule yourself — HeaderGuard monitors your site's security headers continuously and alerts you the moment something changes.
Monitor your site's HTTP security headers for changes. Get alerted the moment a header is dropped, weakened, or misconfigured — before attackers notice it first.
Join the waitlist → Free instant check →