HolicSecurity · Guide
Security headers explained, in plain English
Published 31 October 2026
In short: security headers are instructions your server sends with every page. They tell the browser what the page may load, whether it may be framed, and whether to insist on HTTPS. They're free, take minutes to add, and are easy to forget. Our own sister site was missing several of them, including CSP and HSTS, when we audited it.
Check your headers in 30 seconds
Open a terminal and run:
curl -sI https://example.com
Every line in the output is a response header. Or open your browser's developer tools, go to the Network tab, reload, click the first request and read Response Headers. Compare what you see with the table below.
| Header | What it does | A safe starting value |
|---|---|---|
| Content-Security-Policy | Limits where scripts, styles, images and frames can load from | Depends on your site; see below |
| Strict-Transport-Security | Makes browsers use HTTPS only | max-age=31536000 |
| X-Frame-Options | Stops other sites framing your pages (clickjacking) | DENY or SAMEORIGIN |
| X-Content-Type-Options | Stops the browser guessing file types | nosniff |
| Referrer-Policy | Controls how much of your URL is shared with other sites | strict-origin-when-cross-origin |
| Permissions-Policy | Turns off browser features you don't use | camera=(), microphone=(), geolocation=() |
Content-Security-Policy (CSP)
CSP is a list of sources the browser may load content from. If an attacker manages to inject a <script> into your page, for example through a comment form or a compromised plugin, a good policy stops the browser from running it. It's a second line of defence against cross-site scripting (XSS), not a replacement for fixing the bug.
A strict policy for a site with no third-party scripts looks like this:
Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'
Three tips that save the most time:
- Test first. Send the policy as
Content-Security-Policy-Report-Only. The browser reports violations in the console but blocks nothing, so you can see what would break. - Move inline scripts into files. Inline
<script>blocks need'unsafe-inline', a hash or a nonce.'unsafe-inline'undoes most of the protection, so avoid it. - Structured data is fine. A
<script type="application/ld+json">block is data, not code. Browsers don't execute it, so a strict policy doesn't block it. We checked this in Chrome against the exact policy this site uses.
Strict-Transport-Security (HSTS)
Even if your site redirects HTTP to HTTPS, the very first request can still go out over plain HTTP, where it can be intercepted on a hostile network. HSTS tells the browser to skip HTTP entirely for your domain for the next max-age seconds.
Strict-Transport-Security: max-age=31536000; includeSubDomains
Add includeSubDomains only when every subdomain works over HTTPS, because browsers will refuse plain HTTP on all of them. Start with a short max-age, such as one day, then raise it to a year once nothing breaks. Some top-level domains, such as .dev, are already HTTPS-only in browsers, but sending the header is still good practice.
X-Frame-Options and frame-ancestors
If another site can load your page inside an invisible frame, it can trick visitors into clicking buttons on your page. This is called clickjacking. Block it with either header; sending both covers older browsers:
X-Frame-Options: DENY Content-Security-Policy: frame-ancestors 'none'
Use SAMEORIGIN or frame-ancestors 'self' if your own site frames its pages.
nosniff, Referrer-Policy, Permissions-Policy
X-Content-Type-Options: nosniff
Without it, a browser may decide that an uploaded "image" is really a script and run it. nosniff makes the browser trust the declared content type. There's no reason not to send it.
Referrer-Policy
When a visitor clicks a link to another site, the browser can send the full URL they came from, which may include search terms, IDs or tokens. strict-origin-when-cross-origin sends only your domain to other sites and nothing when leaving HTTPS for HTTP.
Permissions-Policy
Switches off browser features your site never uses, so a malicious script or embedded frame can't ask for them either. For most marketing sites:
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=(), usb=()
Leave out any feature your site does use. For example, remove payment=() if your checkout uses the browser's Payment Request API.
COOP, CORP and CORS
- Cross-Origin-Opener-Policy: same-origin cuts the link between your page and pop-ups or tabs from other sites, which blocks a class of cross-window attacks.
- Cross-Origin-Resource-Policy: same-origin stops other sites embedding your files directly. Don't set it on files you want others to embed, such as public images or fonts.
- Access-Control-Allow-Origin: * (CORS) lets any website read your responses with JavaScript. That's harmless for public static pages but risky on anything that returns private data. Only allow the origins that need it.
Headers you should remove
Headers such as Server: Apache/2.4.41, X-Powered-By: PHP/7.4 or X-Generator announce exact software versions. That doesn't make you vulnerable by itself, but it tells anyone scanning the internet which known CVEs to try. Hide the version, keep the software patched, or both. In nginx use server_tokens off;, in Apache ServerTokens Prod, and in PHP expose_php = Off.
Copy-paste config
Cloudflare Pages and Netlify (_headers file)
This is the real file that serves this site. It works as-is for a static site with no scripts. If your site has scripts, add script-src 'self' and the other sources you use.
/* Content-Security-Policy: default-src 'none'; style-src 'self'; font-src 'self'; img-src 'self'; frame-src 'self'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'; upgrade-insecure-requests Strict-Transport-Security: max-age=31536000; includeSubDomains X-Frame-Options: DENY X-Content-Type-Options: nosniff Referrer-Policy: strict-origin-when-cross-origin Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=(), usb=() Cross-Origin-Opener-Policy: same-origin Cross-Origin-Resource-Policy: same-origin
nginx
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; add_header X-Frame-Options "DENY" always; add_header X-Content-Type-Options "nosniff" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always; server_tokens off;
In nginx, add_header lines in a location block replace the ones from the server block instead of adding to them, so repeat them where needed.
Apache (mod_headers)
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains" Header always set X-Frame-Options "DENY" Header always set X-Content-Type-Options "nosniff" Header always set Referrer-Policy "strict-origin-when-cross-origin" Header always set Permissions-Policy "camera=(), microphone=(), geolocation=()"
Add a CSP once you've tested it in report-only mode. Then run curl -sI again to confirm every header arrives.