# The HTTP security headers every site should send

> What HSTS, CSP, X-Content-Type-Options, X-Frame-Options, Referrer-Policy and Permissions-Policy protect against, with sane starter values.

Author: [Flavio Copes](https://flaviocopes.com/about/) | Published: 2026-08-06 | Topics: [Networking](https://flaviocopes.com/tags/network/) | Canonical: https://flaviocopes.com/http-security-headers/

Every response your server sends can carry a handful of headers that tell the browser "protect my users from this class of attack".

Most sites don't send them. Not because they're hard to add, but because nobody sat down for ten minutes to do it.

Let's fix that. We'll go through the six headers that matter, what each one protects against, and a value you can copy today.

If you want a refresher on response headers in general, I wrote a [full list of HTTP response headers](https://flaviocopes.com/http-response-headers/) a while back.

## Strict-Transport-Security (HSTS)

HSTS tells the browser: "only ever talk to this host over HTTPS, for this many seconds".

```
Strict-Transport-Security: max-age=31536000; includeSubDomains
```

Why does this matter? Without it, a user typing `flaviocopes.com` in the address bar starts with a plain HTTP request. That first request can be intercepted and downgraded by an attacker on the same network. With HSTS, the browser remembers and upgrades to HTTPS before the request ever leaves the machine.

`max-age=31536000` is one year. Start lower (like a day) while you verify everything works on HTTPS, then raise it. `includeSubDomains` extends the rule to all your subdomains.

There's also a `preload` flag, which gets your domain baked into the browsers' built-in HSTS lists. It closes the very first visit gap too. Be careful with it: getting off the preload list takes months, so only add it when you're sure you'll never need HTTP again.

I covered how HTTPS itself works in [the HTTPS protocol](https://flaviocopes.com/https/) if you want the background.

## Content-Security-Policy (CSP)

CSP is the big one. It's a whitelist of where scripts, styles, images and network connections are allowed to come from.

Its main job is stopping **XSS**. Even if an attacker manages to inject a `<script>` tag into your page, the browser refuses to run it because the script's source isn't on your list. I wrote about how those attacks work in the [XSS tutorial](https://flaviocopes.com/xss/).

A strict starting point:

```
Content-Security-Policy: default-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'
```

- `default-src 'self'` — everything loads from your own origin unless a more specific directive says otherwise
- `img-src 'self' data:` — images from your origin, plus inline data URIs
- `style-src 'self' 'unsafe-inline'` — many sites need inline styles; drop `'unsafe-inline'` if yours doesn't

The directive to watch is `script-src`. Avoid `'unsafe-inline'` there — allowing inline scripts is exactly what XSS attackers need, and it defeats most of the point of having a CSP.

CSP breaks things when you get it wrong, so don't deploy it blind. Use the report-only variant first:

```
Content-Security-Policy-Report-Only: default-src 'self'
```

Violations get logged to the console (and to a reporting endpoint if you set one) but nothing is blocked. Watch it for a week, fix the policy, then switch to the enforcing header.

## X-Content-Type-Options

This one is a single fixed value:

```
X-Content-Type-Options: nosniff
```

Without it, browsers sometimes "sniff" the content of a response and guess a MIME type that differs from what your `Content-Type` header says. An attacker can abuse that, for example by uploading a file that your server serves as plain text but the browser decides to execute as JavaScript.

`nosniff` says: trust the declared content type, never guess. There's no downside. Send it everywhere.

## X-Frame-Options and frame-ancestors

These protect against **clickjacking**: an attacker embeds your site in an invisible iframe on their page, positions a fake button over your real one, and tricks users into clicking things on your site without knowing it.

The classic header:

```
X-Frame-Options: DENY
```

`DENY` means nobody can put your page in a frame. `SAMEORIGIN` allows framing only from your own origin.

The modern replacement lives inside CSP:

```
Content-Security-Policy: frame-ancestors 'none'
```

`frame-ancestors` is more flexible — you can allow a specific partner domain, for example. My advice is to send both for now. Old browsers read `X-Frame-Options`, modern ones prefer `frame-ancestors`.

## Referrer-Policy

When a user clicks a link on your site, the browser sends the destination a `Referer` header with the URL they came from. That URL can leak private information: query strings, internal paths, user IDs.

```
Referrer-Policy: strict-origin-when-cross-origin
```

This value is the sensible middle ground. Same-origin navigations get the full URL (useful for your own analytics). Cross-origin ones only get your origin, so `https://flaviocopes.com/private/report?user=123` becomes just `https://flaviocopes.com`. Downgrades to HTTP get nothing.

Avoid `unsafe-url` and `no-referrer-when-downgrade` — both leak full URLs cross-origin.

## Permissions-Policy

This header switches off browser features your site doesn't use:

```
Permissions-Policy: camera=(), microphone=(), geolocation=()
```

The empty parentheses mean "nobody, not even my own pages, can use this". If your site never asks for the camera, why leave it enabled? If someone injects code into your page, or a compromised third-party script tries to grab the microphone, the browser blocks it at the platform level.

List the features you don't need and shut them off.

## A starter block you can copy

Putting it all together:

```
Strict-Transport-Security: max-age=31536000; includeSubDomains
Content-Security-Policy: default-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; frame-ancestors 'none'
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=()
```

The CSP is the only one you'll need to adapt to your site. The other five are safe defaults for almost everyone.

## How to check your own site

The quickest way is curl:

```bash
curl -sI https://flaviocopes.com | sort
```

`-I` sends a HEAD request and prints just the headers.

Then read the output. Which security headers are there? Which are missing? I built a [headers explainer tool](https://flaviocopes.com/tools/headers-explainer/) for exactly this: paste the response headers and it explains each one, flags the missing security headers, and points out info leaks like `Server` or `X-Powered-By` version strings.

That last part is worth a mention. Headers like these:

```
Server: nginx/1.24.0
X-Powered-By: Express
```

tell attackers exactly what software and version you run, which they can match against known vulnerabilities. Remove them or strip the version in production.

## Where to set these

You don't set these headers in your application code, usually. Set them at the layer that serves every response:

- **nginx**: `add_header` in your server block
- **Cloudflare Pages / Netlify**: a `_headers` file in your build output
- **Express**: the `helmet` package sets sensible defaults in one line

Ten minutes of work, and a whole class of attacks stops working against your users.
