A browser will do almost anything a page tells it to: run any script, load inside any frame, send the full address of the page to third parties. Security headers are how a site tells the browser to be stricter on its behalf.
They are also easy to get wrong in a way that breaks a working site. Six headers are worth setting. This guide takes them in the order you should deploy them, safest first.
Three you can turn on today
X-Content-Type-Options
X-Content-Type-Options: nosniff
Browsers have a habit of guessing what a file is from its contents when the declared type looks wrong. That guesswork lets an attacker upload something that claims to be an image and have it run as a script. With nosniff, the browser trusts the Content-Type header and refuses to load scripts and stylesheets served with the wrong type.
The only way this breaks a site is if your server was already sending wrong content types, for example JavaScript served as plain text. Fix the type and the problem goes away.
Referrer-Policy
Referrer-Policy: strict-origin-when-cross-origin
When a visitor follows a link or the page loads a resource from another site, the browser tells that site where the request came from. A full URL can carry things you would not choose to share: search terms, account identifiers, password-reset tokens in the query string.
This value sends the full URL only within your own site. Other sites get just the origin, such as https://example.com/, and nothing at all is sent when moving from HTTPS to plain HTTP. Modern browsers already behave this way by default. Setting it explicitly covers older ones and records the decision. If you want other sites to learn nothing, use same-origin or no-referrer.
Permissions-Policy
Permissions-Policy: camera=(), microphone=(), geolocation=()
This switches off browser features your site does not use. The empty parentheses mean “no origin may use this”, which applies to your own pages and to anything embedded in them. If a third-party script or an injected one tries to request the camera, the browser refuses without prompting the visitor.
List only the features you are sure you don't need. If your site does use one, allow it for your own origin with geolocation=(self). Browser support for this header is uneven, so treat it as an extra layer and not as the only control.
Framing: X-Frame-Options and frame-ancestors
Clickjacking works by loading your page in an invisible frame on the attacker's site and lining up a decoy button over a real one. The visitor thinks they are clicking “play” and are in fact clicking “confirm” in their logged-in session with you. The defense is to forbid other sites from framing your pages.
There are two headers for it. The older one is simple:
X-Frame-Options: SAMEORIGIN
DENY forbids all framing. SAMEORIGIN allows your own pages to frame each other. An old third option for naming an allowed site is obsolete and modern browsers do not honor it.
The newer one is a directive inside Content-Security-Policy:
Content-Security-Policy: frame-ancestors 'self'
'none' matches DENY and 'self' matches SAMEORIGIN. Unlike the old header, it can also list specific sites allowed to embed you, such as frame-ancestors 'self' https://partner.example. Where a browser understands both, frame-ancestors wins. Sending both is fine and covers older browsers.
Strict-Transport-Security
Redirecting HTTP to HTTPS leaves a gap: the very first request still goes out unencrypted, and someone on the same network can intercept it and keep the visitor on a fake HTTP version of your site. HSTS closes the gap. Once a browser has seen the header, it rewrites every future request for your domain to HTTPS before sending it, and it stops letting users click through certificate warnings.
The catch is that the browser remembers this for as long as max-age says, in seconds, and you cannot take it back from a browser that has already stored it. So start small:
Strict-Transport-Security: max-age=300
Five minutes is enough to confirm nothing depends on plain HTTP. Raise it to a day, then a week, and when you are confident, to the long-term value:
Strict-Transport-Security: max-age=31536000; includeSubDomains
That is one year. Add includeSubDomains only after checking that every subdomain serves HTTPS with a valid certificate, including forgotten internal ones, because all of them become unreachable over HTTP. The header is only honored when it arrives over HTTPS, so sending it on HTTP responses does nothing.
There is also a preload flag, used to ask browser makers to build your domain into the browser as HTTPS-only. It protects even the first visit, and it is slow to undo. Leave it out until the long max-age has been in place without trouble. If certificates themselves are the weak point, the guide to how TLS certificates work covers what can go wrong there.
Content-Security-Policy
CSP is the most powerful header and the one most likely to break things. It is a list of rules about where a page may load each kind of resource from. Its main purpose is to limit cross-site scripting: if an attacker manages to inject a script tag into your page, a good policy means the browser refuses to run it.
A reasonable first policy for a site that serves its own assets:
Content-Security-Policy: default-src 'self'; img-src 'self' data:; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'self'
default-src is the fallback for scripts, styles, images, fonts and connections, here limited to your own origin. The rest tighten specific things: no plugins, no changing the page's base URL, forms may only submit to you, and only you may frame the page. The last three do not inherit from default-src, so they have to be written out.
On most real sites this policy will block something. Inline <script> blocks and inline event handlers are refused unless you allow them, and so is every analytics, font or payment script from another origin. The tempting fix is to add 'unsafe-inline' to scripts, which lets injected scripts run too and removes most of the benefit. The better fixes are to move inline code into files, or to mark the inline scripts you trust with a nonce or a hash.
Because of all this, never start by enforcing. Send the same policy under a different header name first:
Content-Security-Policy-Report-Only: default-src 'self'; img-src 'self' data:; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'self'
In report-only mode the browser blocks nothing. It logs every violation to the developer console, and to a reporting endpoint if you configure one. Browse your own site with the console open, add the sources you really need, and repeat until normal use is quiet. Then switch the header name to Content-Security-Policy.
The rollout, in order
- Add
X-Content-Type-Options,Referrer-PolicyandPermissions-Policy. Check the site still works. - Add the framing headers, after confirming who embeds you.
- Add HSTS with a short
max-ageand step it up over a few weeks. - Add CSP in report-only mode. Clear the violations.
- Enforce CSP, and keep watching the reports after each release.
Make sure the headers are on every response, including error pages and redirects.
Checking what your site sends
The security headers grader fetches a URL from your browser and checks for these six headers. Each one present counts as a pass, with either X-Frame-Options or a frame-ancestors directive satisfying the framing check. All six earns an A, and the grade drops a letter for each one missing, down to F at two or fewer. It grades presence, not quality: a weak CSP passes the same as a strict one, though it will tell you when an HSTS max-age is under a year. The HTTP headers viewer shows the status code and the response headers themselves.
Both run entirely in your browser, which limits them. A browser only lets one site read another site's response when that site opts in to cross-origin requests, and even then only the headers the site chooses to expose. Many sites don't opt in, and the tools say so when the read is blocked. For your own site, the dependable view is the Network tab of your browser's developer tools, or the command line:
curl -sI https://example.com