Missing security headers: how do you fix them?
By Md Jewele Islam. 1 November 2026. Updated 3 November 2026.
To fix missing security headers, add them where your site's responses are produced: the nginx or Apache config, your CDN's header rules, or a WordPress plugin if you can't reach the server. Add X-Content-Type-Options and framing protection first, then HSTS with a short lifetime, and put Content-Security-Policy in report-only mode until you know it won't break pages. A missing header is a weakness, not a sign you've been hacked.
What does each header do?
| Header | What it does | What could go wrong |
|---|---|---|
| X-Content-Type-Options: nosniff | Stops the browser guessing a file's type from its contents | Scripts or styles served with the wrong type get blocked |
| X-Frame-Options | Controls who can show your page in a frame | Breaks a legitimate embed on another site |
| Strict-Transport-Security (HSTS) | Tells browsers to use only HTTPS for your site | Certificate errors can't be clicked past while it's active |
| Content-Security-Policy (CSP) | Limits what the page may load, which helps guard against cross-site scripting | A strict policy can stop scripts, styles or images from loading |
None of these stops someone logging in with a stolen password or exploiting an old plugin. They close specific browser-side gaps, and that's all.
How do you see which headers your site sends now?
curl -sSI https://sitehealthradar.com/ \
| grep -iE '^(strict-transport-security|content-security-policy|x-frame-options|x-content-type-options)'
Real output, run on 1 November 2026 against this site's own domain:
content-security-policy: default-src 'self'; script-src 'self'; style-src 'self'; font-src 'self'; img-src 'self' data:; connect-src 'self'; form-action 'self'; base-uri 'none'; object-src 'none'; frame-ancestors 'none'
x-content-type-options: nosniff
x-frame-options: DENY
strict-transport-security: max-age=31536000
The same command against example.com that day printed nothing, because that page's response had none of these four. An empty result is what "missing" looks like.
Headers are per response, so check more than your homepage. A login page, a checkout or an API path can behave differently.
What lines do you add on nginx?
Put these in the server block for your HTTPS site, using add_header:
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Strict-Transport-Security "max-age=300" always;
add_header Content-Security-Policy-Report-Only "default-src 'self'; frame-ancestors 'self'" always;
Then sudo nginx -t, and if that passes, sudo systemctl reload nginx.
These four lines were tested on 1 November 2026 on a throwaway local nginx 1.31.8 over HTTPS with a test certificate. nginx -t passed, and all four headers came back on a 200 and on a 404. nginx -s reload was used there, because that machine has no systemctl. A local server can't show how your own pages react, so try a staging site first.
Two rules from the docs catch people out. Both behaved as documented in that test:
- Use
always. Without it, nginx adds the header only for certain status codes, such as 200 and 301, and not for error pages. In the test, a line withoutalwayswas there on the 200 and gone on the 404. - A
locationorserverblock with its ownadd_headerloses the parent's headers. Directives are inherited only if the current level has none. So a line inhttpvanishes in anyserverblock that sets a header of its own. In the test, addingCache-Controlin alocationremoved all four security headers from that path. Either repeat them there, or on nginx 1.29.3 and later addadd_header_inherit merge;, which brought them back.
What lines do you add on Apache?
Using mod_headers, which has to be enabled:
Header always set X-Content-Type-Options "nosniff"
Header always set X-Frame-Options "SAMEORIGIN"
Header always set Strict-Transport-Security "max-age=300"
Header always set Content-Security-Policy-Report-Only "default-src 'self'; frame-ancestors 'self'"
These exact four lines were tested on 1 November 2026 on a throwaway local Apache 2.4.67 over HTTPS with a test certificate. httpd -t returned Syntax OK, and all four headers came back on a 200 and on a 404. With mod_headers not loaded, the config test failed with Invalid command 'Header', so that's the error to look for. A local server can't show how a real browser or your own site reacts.
What if you're on WordPress or shared hosting?
You may not have access to the server config. In that case:
.htaccess. Apache allowsHeaderthere only if the host'sAllowOverrideincludesFileInfo. On the same test server, the four lines above worked on a 200 and a 404 withAllowOverride FileInfo. WithAllowOverride None, they were silently ignored, with no error at all. So check withcurlafterwards, or ask your host. And if your host runs nginx,.htaccessisn't read.- Your CDN. Check whether it has response header rules. On Cloudflare they're called Response Header Transform Rules, and they weren't tested for this guide. For other CDNs, read their docs.
- A plugin. Use a security or headers plugin you trust. A plugin that sends headers from PHP can only touch responses WordPress generates, and WordPress's standard
.htaccessrules let the web server hand out existing files, such as images and CSS, without running WordPress at all. That's our reasoning from those rules, and no plugin was tested. Some may write to.htaccessinstead. So check an image or a CSS file as well as a page.
Whichever you pick, set each header in one place. On a test nginx proxying another server, with both setting X-Frame-Options, the response carried both lines: DENY and SAMEORIGIN. Browsers don't treat every duplicate the same way. For HSTS, the first one counts. For CSP, all the policies apply. Either way you'll be debugging the wrong layer. On Cloudflare, a rule that "sets" a header overwrites it, while one that "adds" keeps the existing one.
How do you turn on HSTS safely?
Be careful with this one, because browsers remember it.
- Make sure HTTPS works on the whole site, and that certificate renewal is working.
- Start with
max-age=300, which is five minutes. - Raise it in stages, as the HSTS preload site suggests:
max-age=604800(a week), thenmax-age=2592000(a month). It says to check for broken pages at each stage and wait the full max-age before moving on. After that,max-age=31536000is a year. - Add
includeSubDomainsonly when every subdomain works on HTTPS. An oldmail.orshop.on plain HTTP will stop working for anyone whose browser has the rule. That preload site's examples include it from the start, because it's aimed at preloading, and it tells you to check every subdomain first. - Leave
preloadoff unless you're sure. The preload list warns that getting removed is slow, and that it takes months to reach users.
Browsers ignore HSTS sent over plain HTTP, so testing it on an HTTP-only staging site proves nothing. To back out, send max-age=0, which tells a browser to forget the rule. That only reaches browsers that can still connect to you over HTTPS.
The tests above show the server sends the header. How a browser behaves once it has the rule comes from those docs and wasn't tested here.
How do you add Content-Security-Policy without breaking the site?
Start with Content-Security-Policy-Report-Only. The browser notes what the policy would have blocked but doesn't block it.
- Add the report-only line above.
default-src 'self'is a strict starting point, and a site with analytics, chat widgets, web fonts or inline scripts will show plenty of violations. That's expected. - Open your main pages with the browser's developer console open, and read what would be blocked. In a test on Chromium 156, a page with an inline script under that exact header ran the script and logged the violation in the console, as an info message rather than an error. So don't filter info messages out. Other browsers weren't tested.
- If you want reports sent somewhere, add a
report-toendpoint. MDN goes further and says the header has no effect withoutreport-to, which doesn't match what the console did in that test. Our reading is that it means no reports get sent. If you need a record, set one up. - Add the sources you trust. Avoid
'unsafe-inline'as a shortcut. MDN warns that it "defeats much of the purpose of having a CSP". It suggests nonces or hashes for inline scripts instead. - When the important pages come up clean, rename the header to
Content-Security-Policy, then test again, including checkout, forms and logins.
Report-only needs to be sent as a real header. MDN says it isn't supported in a <meta> tag.
A scanner may still call CSP missing while you're in report-only mode. Ours does, and that's fair, because nothing is being enforced yet.
X-Frame-Options or frame-ancestors?
Either one protects you from being framed. MDN points to frame-ancestors for more comprehensive options. X-Frame-Options takes DENY (nobody) or SAMEORIGIN (only your own site). ALLOW-FROM is obsolete, and modern browsers ignore the whole header if they see it. If another site needs to embed you, use frame-ancestors and list that site, and drop X-Frame-Options, since it has no way to name another site.
How do you know it worked?
Run the curl command again, on your homepage and on one or two inner pages. Then click through the parts of the site that make you money. A header that's present and a working checkout are two separate tests. X-Content-Type-Options can bite here: in the same Chromium test, a script served as text/plain was refused once nosniff was on.
If you're behind a CDN, remember what you're seeing is what the CDN sends. Clear its cache if an old response comes back.
If you'd rather hand this over, it's the sort of thing we fix on request, quoted per job after a look.
Which headers is your site missing?
The free security headers checker reads one ordinary response from your homepage and looks for HSTS, X-Content-Type-Options, framing protection and Content-Security-Policy, plus whether the response announces a version number. Each missing one comes with a suggested fix. It looks at the homepage only, and it counts only an enforcing CSP, so report-only still shows as missing. Its HSTS suggestion includes includeSubDomains, so read the step above before you copy that part.
Sources were read on 1 November 2026 and checked again on 3 November 2026. Vendor pages change, so check them if you're reading this much later.