Security headers, explained by what our scanner sees
Five short lines in your server response stop most clickjacking, script injection and downgrade attacks. Most sites we scan send none of them. Here is what each one does, in plain terms, and how to add them on Apache, Nginx and Cloudflare.
When a browser asks your server for a page, the server sends the page plus a set of headers, short lines of text the visitor never sees. Five of those lines tell the browser how to protect the visitor while they are on your site. Leave them out and the browser falls back to its most permissive behaviour.
Our scanner checks for all five. Across the sites people have run through it so far, the most common result is zero out of five. That includes sites with a padlock, a firewall plugin and a recent redesign. Headers are a server setting, and nobody sets them unless someone asks.
The five, in plain terms
Strict-Transport-Security, usually called HSTS. Tells the browser: for the next year, only ever talk to this site over HTTPS, even if someone types http. Without it, the very first request to your site can be intercepted on a public network before the redirect to HTTPS happens.
Strict-Transport-Security: max-age=31536000; includeSubDomains
Only send this once every part of your site works over HTTPS, because browsers remember it.
X-Content-Type-Options. Stops the browser guessing what a file is. Without it, an uploaded file that claims to be an image can be run as a script.
X-Content-Type-Options: nosniff
X-Frame-Options. Stops other sites loading your pages inside an invisible frame and tricking visitors into clicking things on it. That trick is called clickjacking, and it still works on most sites.
X-Frame-Options: SAMEORIGIN
Referrer-Policy. Controls how much of your address is passed along when a visitor clicks a link to another site. Without it, full addresses, including search terms or account pages, leak to whoever you link to.
Referrer-Policy: strict-origin-when-cross-origin
Content-Security-Policy, or CSP. The big one. It lists where scripts, styles, images and fonts are allowed to load from. Injected script from an unknown domain simply does not run. It is also the one that breaks things if written carelessly, because it will block your own analytics or fonts if you forget to list them.
A starting policy for a typical site that uses Google Fonts:
Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https:; frame-ancestors 'self'; base-uri 'self'; form-action 'self'
Yes, the ‘unsafe-inline’ part weakens it. A policy with that allowance is still far better than no policy, and it is where nearly every real site has to start. Tighten it later.
A sixth line, Permissions-Policy, turns off camera, microphone and location access for your pages. It costs nothing to add:
Permissions-Policy: camera=(), microphone=(), geolocation=()
How to add them
Apache, which is most shared hosting. In the .htaccess file at the root of the site:
<IfModule mod_headers.c>
Header always set X-Content-Type-Options "nosniff"
Header always set X-Frame-Options "SAMEORIGIN"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Permissions-Policy "camera=(), microphone=(), geolocation=()"
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains" env=HTTPS
</IfModule>
Nginx. In the server block, then reload:
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
Cloudflare. Under Rules, open Transform Rules and add a response header rule with each line. It then applies to everything behind Cloudflare without touching the server. HSTS also has its own switch under SSL/TLS, Edge Certificates.
WordPress with no server access. A small plugin, or a few lines in a must-use plugin, can send them with PHP. It works, but the server or Cloudflare is the better place, because that also covers files WordPress never touches.
What happened on our own site
We built this site, ran it through our own scanner, and it failed the headers section. None of the five were set. It took twenty minutes to add them and check that nothing broke. The policy blocked one thing we had not expected: WordPress’s own emoji script, which starts a background worker the policy did not allow. We removed the script rather than loosen the policy.
Check yours. Paste the address into the scanner and the headers section shows exactly which of the five are missing.
See where your own site stands.
The scanner checks the things this guide talks about, in about ten seconds, no signup.
More from the guides
Hacked WordPress: the cleanup order that holds
Most hacked sites get cleaned twice. The first time removes what you can see. Here is the full order we use so the second time is never needed: contain, find the door, clean, rotate, patch, watch.
Is my website hacked? Twelve signs, and a five-minute check
Most hacked sites look normal to their owners for weeks. The signs show up in Google, in your inbox and on other people's phones first. Here are the twelve we check, and a five-minute routine that catches most of them.
Deceptive site ahead: what it means and the fix order
Chrome is not warning about your design. Google Safe Browsing found phishing or malware on your domain. Here is what the red screen means, how to find the cause, and how to get the flag removed without it coming back.