# HTTP Security Headers Checklist: Values and Configs

> Which HTTP security headers to send and which to drop: starting values for CSP, HSTS and more, with nginx, Apache, Express, Next.js and Cloudflare config.

Source: https://howhttpworks.com/guides/http-security-headers-checklist
Last reviewed: 2026-10-04

> **TL;DR:** Send `Strict-Transport-Security`, `X-Content-Type-Options: nosniff`, `Referrer-Policy: strict-origin-when-cross-origin`, a frame-protection header and a Content-Security-Policy you ship as Report-Only first; add `Permissions-Policy` and `Cross-Origin-*` headers where your site allows. Do not send `Expect-CT`, `Public-Key-Pins` or `Feature-Policy`, and set `X-XSS-Protection` only to `0` or leave it out. Check your result with the [Site Verifier](https://howhttpworks.com/tools/site-verifier).

## The checklist

Starting values, not universal answers. Each links to the page that covers the header in depth.

| Header | Recommended starting value | What it prevents |
| --- | --- | --- |
| [Strict-Transport-Security](https://howhttpworks.com/headers/strict-transport-security) | `max-age=300` first, then `max-age=31536000; includeSubDomains` | SSL-stripping and downgrade to HTTP after the first HTTPS visit |
| [Content-Security-Policy](https://howhttpworks.com/headers/content-security-policy) | Report-Only starter below | Injected scripts running (XSS impact), clickjacking via `frame-ancestors`, data exfiltration to unlisted hosts |
| [X-Content-Type-Options](https://howhttpworks.com/headers/x-content-type-options) | `nosniff` | Browsers guessing a script or stylesheet from a response served with the wrong `Content-Type` |
| [Referrer-Policy](https://howhttpworks.com/headers/referrer-policy) | `strict-origin-when-cross-origin` | Full URLs (with tokens or IDs in the path and query) leaking to other origins in `Referer` |
| [X-Frame-Options](https://howhttpworks.com/headers/x-frame-options) | `DENY`, or `SAMEORIGIN` if you frame yourself; pair with CSP `frame-ancestors` | Clickjacking: your page being framed by another site |
| [Permissions-Policy](https://howhttpworks.com/headers/permissions-policy) | `camera=(), microphone=(), geolocation=(), payment=()` for features you do not use | Your page, or a third-party iframe on it, using powerful browser features |
| [Cross-Origin-Opener-Policy](https://howhttpworks.com/headers/cross-origin-opener-policy) | `same-origin` (test OAuth and payment popups first) | Cross-origin windows holding a reference to yours via `window.opener`; prerequisite for cross-origin isolation |
| [Cross-Origin-Resource-Policy](https://howhttpworks.com/headers/cross-origin-resource-policy) | `same-site` or `same-origin` on resources not meant to be embedded | Other sites embedding your images, scripts or JSON responses (Spectre-class and hotlinking exposure) |
| [Cross-Origin-Embedder-Policy](https://howhttpworks.com/headers/cross-origin-embedder-policy) | `require-corp`, only if you need cross-origin isolation (`SharedArrayBuffer`) | Loading cross-origin resources that have not opted in; breaks third-party embeds, so skip it by default |

Cookies carry their own flags rather than headers of this kind: see [Cookie Security](https://howhttpworks.com/guides/cookie-security) for `Secure`, `HttpOnly` and `SameSite`.

Notes that save debugging time:

- **HSTS is sticky.** A browser that has seen `max-age=31536000` refuses plain HTTP for your host for a year. Start at `max-age=300`, confirm every subdomain you add with `includeSubDomains` serves HTTPS, then raise it. Add `preload` only after reading the preload requirements; removal from the list takes months.
- **Set HSTS only over HTTPS.** Browsers ignore it on HTTP responses.
- **`X-Frame-Options` has no multi-origin allow-list.** The `ALLOW-FROM` value is obsolete; use CSP `frame-ancestors https://partner.example` when a specific partner may frame you.
- **COOP `same-origin` breaks popup flows** that rely on `window.opener` or `window.closed`, such as some OAuth and payment pop-ups. Test the login flow before shipping.
- **A `Referrer-Policy` of `no-referrer`** also removes `Referer` for your own analytics and CSRF checks that read it. `strict-origin-when-cross-origin` is the browser default in current browsers, but setting it explicitly protects older clients and documents intent.

## What not to send

| Header | Why to drop it |
| --- | --- |
| `X-XSS-Protection: 1; mode=block` | The legacy filter could be abused to disable parts of a page or leak information, and Chrome removed its auditor. Send `X-XSS-Protection: 0` to switch off any remaining legacy filter, or omit it. See [X-XSS-Protection](https://howhttpworks.com/headers/x-xss-protection). |
| `Expect-CT` | Existed to enforce Certificate Transparency before it became a baseline requirement for publicly trusted certificates. Browsers no longer act on it and MDN lists it as deprecated. |
| `Public-Key-Pins` (HPKP) | Pinning the wrong key could lock users out of your site for months. Chrome removed support and no current browser honors it. |
| `Feature-Policy` | Replaced by `Permissions-Policy`, which has different syntax. See [Permissions-Policy vs Feature-Policy](https://howhttpworks.com/compare/permissions-policy-vs-feature-policy). |
| `Server: nginx/1.27.0`, `X-Powered-By: Express` | Not a protection, but version strings help attackers pick exploits. Use `server_tokens off;` in nginx, `ServerTokens Prod` in Apache and remove `X-Powered-By`. |
| `Access-Control-Allow-Origin: *` with credentialed responses | Browsers reject the wildcard when credentials are included, and reflecting arbitrary `Origin` values is worse. Use an explicit allow-list. See the [CORS guide](https://howhttpworks.com/guides/cors). |

## A CSP starter that will not break most sites

Start with a policy that is strict about the dangerous things (plugins, `<base>`, form targets, framing) and tolerant about the common ones (inline styles, images from HTTPS hosts). Scripts are the part that needs your attention.

```http
Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'self'; report-to csp-endpoint; report-uri /csp-reports
Reporting-Endpoints: csp-endpoint="https://example.com/csp-reports"
```

`report-to` uses the endpoint named in `Reporting-Endpoints`. `report-uri` is deprecated in the spec but still the one that works in browsers that lack the Reporting API, so most deployments send both.

This starter will report, and once enforced block, inline `<script>` blocks, inline event handlers (`onclick=`), third-party script hosts and `eval`. That is deliberate: those are what an XSS attack uses.

**Rollout plan:**

1. **Report-Only for one full release cycle.** Collect reports. Filter noise from browser extensions (`chrome-extension://`, `moz-extension://` as the `blocked-uri`).
2. **Allow-list the legitimate third parties** by exact host in `script-src`, `img-src`, `connect-src` and so on. Avoid wildcards such as `https:` in `script-src`.
3. **Replace inline scripts** with external files, or use per-response nonces (`script-src 'nonce-<random>' 'strict-dynamic'`) if you cannot remove them. A nonce must be unpredictable and different on every response, so it cannot be set statically in an nginx file.
4. **Switch to enforcing** by renaming the header to `Content-Security-Policy`, keeping the same value. Keep a Report-Only copy of the next, stricter policy running next to it.
5. **Paste the final policy into the [CSP Evaluator](https://howhttpworks.com/tools/csp-evaluator)** to catch weak sources such as `'unsafe-inline'` in `script-src` or wildcard hosts.

For nonces, hashes, `strict-dynamic` and the exact console errors, see the [Content-Security-Policy page](https://howhttpworks.com/headers/content-security-policy).

## Config blocks

Every block below sets the same baseline. Replace the CSP with your own tested policy and keep the Report-Only value until you are ready.

### nginx

```nginx
server {
    listen 443 ssl;
    server_name example.com;
    server_tokens off;

    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
    add_header X-Frame-Options "DENY" always;
    add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
    add_header Cross-Origin-Opener-Policy "same-origin" always;
    add_header Content-Security-Policy-Report-Only "default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'" always;

    location /api/ {
        # GOTCHA: this one add_header means NONE of the server-level
        # headers above are sent for /api/ responses.
        add_header Cache-Control "no-store" always;
        proxy_pass http://app;
    }
}
```

Because of that `location /api/` block, every security header above disappears on API responses. nginx inherits `add_header` from the previous level only if the current level has none. The fix is to repeat them, and the clean way is a snippet:

```nginx
# /etc/nginx/snippets/security-headers.conf
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header X-Frame-Options "DENY" always;
```

```nginx
location /api/ {
    include snippets/security-headers.conf;
    add_header Cache-Control "no-store" always;
    proxy_pass http://app;
}
```

Separately, without `always` nginx adds headers only on 200, 201, 204, 206, 301, 302, 303, 304, 307 and 308. Your 404 and 502 pages would go out unprotected. Also remember that upstream response headers pass through, so a header your app already sets can appear twice; use `proxy_hide_header` to drop the duplicate. Verify with `curl -sI https://example.com/api/ping`, not only the homepage.

### Apache

Requires `mod_headers`.

```apache
ServerTokens Prod
ServerSignature Off

<IfModule mod_headers.c>
    Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
    Header always set X-Content-Type-Options "nosniff"
    Header always set Referrer-Policy "strict-origin-when-cross-origin"
    Header always set X-Frame-Options "DENY"
    Header always set Permissions-Policy "camera=(), microphone=(), geolocation=()"
    Header always set Cross-Origin-Opener-Policy "same-origin"
    Header always set Content-Security-Policy-Report-Only "default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'"
    Header always unset X-Powered-By
</IfModule>
```

`Header always` applies to error responses too, which plain `Header set` does not.

### Express with Helmet

```javascript
import express from 'express'
import helmet from 'helmet'

const app = express()

app.use(
  helmet({
    contentSecurityPolicy: {
      useDefaults: true,
      reportOnly: true,
      directives: {
        'script-src': ["'self'"],
        'img-src': ["'self'", 'data:', 'https:']
      }
    },
    crossOriginEmbedderPolicy: false
  })
)
```

Helmet's README lists these defaults: `Content-Security-Policy` (a restrictive policy including `default-src 'self'` and `upgrade-insecure-requests`), `Cross-Origin-Opener-Policy: same-origin`, `Cross-Origin-Resource-Policy: same-origin`, `Origin-Agent-Cluster: ?1`, `Referrer-Policy: no-referrer`, `Strict-Transport-Security: max-age=31536000; includeSubDomains`, `X-Content-Type-Options: nosniff`, `X-DNS-Prefetch-Control: off`, `X-Download-Options: noopen`, `X-Frame-Options: SAMEORIGIN`, `X-Permitted-Cross-Domain-Policies: none` and `X-XSS-Protection: 0`, and it removes `X-Powered-By`. `Cross-Origin-Embedder-Policy` is not set by default.

Two defaults commonly surprise people. `Cross-Origin-Resource-Policy: same-origin` blocks other origins, including your own separate frontend or CDN domain, from loading your images and API responses; override with `crossOriginResourcePolicy: { policy: 'cross-origin' }` where needed. And the default CSP is enforcing, which is why the example sets `reportOnly: true` for the first rollout. Helmet does not set `Permissions-Policy`; set it with `res.setHeader` or a small middleware.

### Next.js

`headers()` in `next.config.js` covers static values. Policies that need a per-request nonce must be set in middleware instead.

```javascript
// next.config.js
const securityHeaders = [
  { key: 'Strict-Transport-Security', value: 'max-age=31536000; includeSubDomains' },
  { key: 'X-Content-Type-Options', value: 'nosniff' },
  { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
  { key: 'X-Frame-Options', value: 'DENY' },
  { key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=()' }
]

module.exports = {
  poweredByHeader: false,
  async headers() {
    return [{ source: '/(.*)', headers: securityHeaders }]
  }
}
```

If you deploy behind another proxy that already sets these headers, you will send duplicates; set them in one place.

### Cloudflare

For a site proxied through Cloudflare, add the headers at the edge without touching the origin. In the dashboard use Rules, Transform Rules, Modify Response Header, and add a "Set static" operation per header. Cloudflare also has its own HSTS toggle in the SSL/TLS settings.

For anything conditional, a Worker can set them in code:

```javascript
export default {
  async fetch(request) {
    const response = await fetch(request)
    const secured = new Response(response.body, response)
    secured.headers.set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains')
    secured.headers.set('X-Content-Type-Options', 'nosniff')
    secured.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin')
    secured.headers.set('X-Frame-Options', 'DENY')
    secured.headers.set('Permissions-Policy', 'camera=(), microphone=(), geolocation=()')
    secured.headers.delete('X-Powered-By')
    return secured
  }
}
```

Copying the response into a new `Response` is required: the headers on the original returned from `fetch()` are immutable.

## Verify

```bash
curl -sI https://example.com | grep -iE 'strict-transport|content-security|x-content-type|referrer-policy|x-frame|permissions-policy|cross-origin'
```

Check a 404 and an API route as well, since those are where inheritance bugs hide. For a graded report on a live URL, run the [Site Verifier](https://howhttpworks.com/tools/site-verifier); for the policy itself, use the [CSP Evaluator](https://howhttpworks.com/tools/csp-evaluator).
