HTTP security headers are response headers that tell the browser to apply protections your site would otherwise have to build itself: stay on HTTPS, run only scripts from sources you trust, don’t guess file types, don’t let other sites frame the page, and share less about where a visitor came from. Six headers cover most of it: Strict-Transport-Security, Content-Security-Policy, X-Content-Type-Options, X-Frame-Options (or the CSP’s frame-ancestors), Referrer-Policy and Permissions-Policy. Most take one line of server configuration. Content-Security-Policy takes planning, so start it in Report-Only mode.
The six headers at a glance
| Header | Protects against | A value to start with |
|---|---|---|
| Strict-Transport-Security | Connections downgraded to plain HTTP | max-age=31536000 |
| Content-Security-Policy | Injected scripts (cross-site scripting) | A policy tested in Report-Only first |
| X-Content-Type-Options | Browsers running a file as another type | nosniff |
| X-Frame-Options or frame-ancestors | Clickjacking through frames | SAMEORIGIN and frame-ancestors 'self' |
| Referrer-Policy | Full addresses leaking to other sites | strict-origin-when-cross-origin |
| Permissions-Policy | Unused browser features being requested | camera=(), microphone=(), geolocation=() |
Strict-Transport-Security (HSTS)
Even with a redirect from HTTP to HTTPS, a visitor who types your domain makes the first request over plain HTTP, where it can be read or changed on the way. HSTS tells the browser to use HTTPS for your domain from then on, for as long as max-age says, in seconds:
Strict-Transport-Security: max-age=31536000
- Start short. Browsers obey HSTS until it expires, so a mistake sticks. Try
max-age=300for a few days, then raise it to a year (31536000). includeSubDomainsapplies the rule to every subdomain. Add it only when all of them, including old staging and mail hosts, work over HTTPS.preloadasks to have the domain built into browsers through hstspreload.org, which requires a max-age of at least a year andincludeSubDomains. Removal takes a long time, so treat it as permanent.- Browsers ignore HSTS on plain HTTP responses. Send it over HTTPS, and keep an eye on the certificate: with HSTS, an expired certificate can’t be clicked past.
Content-Security-Policy
A Content Security Policy lists where the page may load scripts, styles, images, fonts and frames from, and where forms may post to. If an attacker gets a script into the page, through a comment field or a compromised plugin, the browser refuses to run it unless the policy allows its source. It is the most effective of the six and the easiest to get wrong, since a policy that is too strict breaks your own analytics, chat widget or payment form.
Start with Report-Only
Send the policy as Content-Security-Policy-Report-Only first. The browser enforces nothing but reports every violation to the address in report-uri (or the newer report-to), and logs it in the console. A starting point for a site that serves its own files:
Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; font-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'self'; report-uri /csp-report
Browse the site, collect the reports for a week or two, and add the sources you really use, such as your analytics or payment provider. When the reports only show things you want blocked, send the same policy as Content-Security-Policy.
Inline scripts: nonces, not unsafe-inline
A strict policy blocks inline <script> blocks and onclick attributes, since an injected script looks the same. Adding 'unsafe-inline' makes the problem go away and most of the protection with it. Instead, give each response a random nonce, list it in the policy as 'nonce-…', and put the same value on your own script tags; move inline event handlers into script files. Tag managers that inject their own scripts need extra care, since each tag they add must also be allowed.
X-Content-Type-Options
Browsers used to guess a file’s type from its content when the server’s Content-Type looked wrong. That let an uploaded “image” run as a script. X-Content-Type-Options: nosniff turns the guessing off. It has one valid value and no known downside, as long as your server sends correct content types, so send it on every response.
X-Frame-Options and frame-ancestors
In a clickjacking attack, another site shows your page in an invisible frame and tricks visitors into clicking buttons on it. X-Frame-Options: DENY stops all framing, and SAMEORIGIN allows only your own pages. The CSP directive frame-ancestors does the same with more control, such as a list of partner sites, and takes precedence in browsers that support it. Send both: frame-ancestors for current browsers, X-Frame-Options for older ones. The old ALLOW-FROM value is no longer supported, so use frame-ancestors for anything beyond your own origin.
Referrer-Policy
When a visitor follows a link, the browser tells the next site where they came from in the Referer header. Without limits, that can be the full address, including a search query, a reset token or an order number in the URL. strict-origin-when-cross-origin sends the full address within your own site, only the domain to other sites, and nothing from HTTPS to HTTP. It is what current browsers do when the header is missing, but sending it makes the choice explicit. Use no-referrer on pages with private data in their URLs, and avoid unsafe-url, which sends everything everywhere.
Permissions-Policy
Permissions-Policy, formerly Feature-Policy, turns off browser features your site doesn’t use, for the page and for every frame embedded in it. An empty list, (), means nobody may use the feature:
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=(), usb=()
Leave out the features you do use, or allow them for your own origin with (self), for example payment=(self) on a shop with a payment button.
Headers you can leave out
- X-XSS-Protection controlled a filter that current browsers have removed. Leave it out, or send
0. - Expect-CT and Public-Key-Pins are obsolete. Pinning could lock visitors out of a site, which is why browsers dropped it.
- Server and X-Powered-By aren’t security headers, but exact version numbers in them help attackers pick an exploit. Trim them where your server allows.
Where to set them
Set the headers in one place, as close to the edge as you can: the web server, the CDN or the application’s middleware. In nginx and Apache:
# nginx (in the server block)
add_header Strict-Transport-Security "max-age=31536000" always;
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;
# Apache (.htaccess or virtual host, with mod_headers)
Header always set X-Content-Type-Options "nosniff"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Two nginx details catch people out. Without always, the headers are only added to successful responses, not to error pages. And a location block with any add_header of its own drops all the ones from the server block, so a single cache header for images can silently remove your security headers there. Also watch for the same header set twice, by the server and by the application or CDN: browsers handle duplicates differently from header to header, so pick one owner for each.
How to check your headers
From a terminal, curl -sI https://example.com prints the response headers. Check the final address after redirects, an error page and a static file too, since each may be served by a different part of your stack. In a browser, the Network tab of the developer tools shows the headers for each request, and the console shows CSP violations.
Or use our free HTTP header checker. It lists every header a URL sends after following redirects, marks which of the six security headers are missing, flags a few values that undo them, such as a short HSTS max-age, a CSP that is only Report-Only, or unsafe-url, and shows how the page is cached and where the time goes. It checks that the headers are there; it doesn’t grade what a Content Security Policy allows, so review that yourself.
Keep them from disappearing
Security headers rarely break loudly. They vanish when a site moves to a new server, a CDN is switched, a plugin is updated or someone adds a location block, and the site keeps working, so nobody notices. An HTTP monitor in Uptimecheck uptime monitoring can check response headers on every request, up to 10 response checks per monitor, for example:
Strict-Transport-Securityexists.X-Content-Type-Optionsequalsnosniff.Content-Security-Policycontainsframe-ancestors.
Every check must hold for the monitor to count as up, so a missing header turns it down, a second check confirms it, and you get an alert. Put the header checks on a monitor of their own if you don’t want them to count against the page’s uptime. Checks run as often as every minute from Stockholm, Sweden; see how to monitor an API endpoint for setting up response checks, and the website maintenance checklist for the rest of the routine.
Find out the day a header goes missing. Try Uptimecheck free for 14 days, no credit card required.