HEADER-BY-HEADER CHECK

Security headers check: how to find and fix missing HTTP security headers

Security headers are instructions your server sends with its responses: which resources a page may load, whether the browser must insist on HTTPS, who may put the page in a frame. Here is how to read them yourself, what each one does, safe starting values and a rollout order that lets you test each change before you enforce it.

Concept illustration of a mint light beam passing through five patterned glass panes toward an ivory archway, while a copper lens inspects one of the panes.
Concept illustration

Every response from your site carries headers the visitor never sees. Some of them are security instructions for the browser: load scripts only from these places, use HTTPS for this host, do not let other sites frame this page, keep this cookie away from JavaScript. When they are missing, the browser falls back to its more permissive defaults. When they are wrong, sign-in or checkout can break.

This guide covers how to read the headers your site actually sends, what each one does and which values are safe to start with. It includes a baseline configuration for nginx and for static hosts such as Cloudflare Pages and Netlify, and the mistakes that leave some responses without headers. All examples use example.com.

Scope of this guide

The free Launch Readiness Snapshot reads the headers of one response: the home page of the origin you enter, after up to five redirects. It mostly checks whether each header is present, not whether the values suit your site, and it is not a pentest.

1. Check your headers yourself: DevTools and curl

In Chrome or Edge, open DevTools, select the Network panel and reload the page. Click the first request (the HTML document), open the Headers tab and scroll to Response Headers. Tick Preserve log to keep requests across page loads and redirects, and Disable cache to get a fresh response instead of a cached one (Chrome DevTools network reference). The Network Monitor in Firefox works the same way.

From a terminal, curl prints exactly what the server sends:

curl -sS -D - -o /dev/null https://example.com/

Add -L to follow redirects and see the headers of every hop. On Windows, call curl.exe and use -o NUL. curl -I is shorter, but it sends a HEAD request instead of GET, and some applications handle HEAD on a different code path, so confirm what you find with a GET.

Repeat the check for the http:// address (it should answer with a 301 or 308 redirect to HTTPS), the other host name (www or the bare domain), a page that does not exist, a sign-in page, an API response and a static file. The application, the web server and the CDN often set different headers on each of these, and only what reaches the browser counts.

2. What each header does and a safe value to start with

HeaderWhat it doesSafe starting value
Content-Security-PolicyLimits where scripts, styles, frames and other resources may load fromA draft policy, sent first as Content-Security-Policy-Report-Only
CSP frame-ancestorsDecides which sites may embed the page in a frame (clickjacking defense)frame-ancestors 'self' or 'none'
X-Frame-OptionsLegacy framing control, kept as a fallback for older browsersSAMEORIGIN or DENY, matching frame-ancestors
Strict-Transport-SecurityTells the browser to use only HTTPS for this host for max-age secondsmax-age=300, raised in stages
X-Content-Type-OptionsStops MIME-type guessing; blocks scripts and stylesheets served with the wrong typenosniff
Referrer-PolicyControls how much of the page URL is sent to other sites in the Referer headerstrict-origin-when-cross-origin
Permissions-PolicySwitches off browser features such as the camera for the page and its framescamera=(), microphone=(), geolocation=()
Cross-Origin-Opener-PolicyIsolates your window from cross-origin pop-ups and from the page that opened itsame-origin, or same-origin-allow-popups for OAuth or payment pop-ups
Set-Cookie attributesLimit how session cookies travel and who can read themSecure; HttpOnly; SameSite=Lax
X-XSS-ProtectionControlled a filter in old browsers; now deprecatedRemove it, or send 0

With nosniff, the browser trusts the declared Content-Type and blocks scripts and stylesheets that arrive with the wrong one, so check those types before you switch it on. Without a Referrer-Policy header, current browsers already use strict-origin-when-cross-origin; OWASP still suggests sending it explicitly for older browsers, and no-referrer is stricter if nothing you rely on needs the referrer. In Permissions-Policy, () disables a feature in the page and in every frame inside it; allow a feature only for the origin of the widget that needs it. COOP same-origin helps against cross-origin leak attacks (XS-Leaks) but can break sign-in or payment pop-ups served from another origin.

3. Content-Security-Policy and frame-ancestors

CSP tells the browser where a page may load scripts, styles, images, frames and connections from. It limits what injected script can do; it does not replace escaping and validating input. The MDN CSP guide recommends a strict CSP built on a fresh nonce per response, or on hashes, rather than a long list of allowed hosts. 'unsafe-inline' defeats much of the purpose, and the browser ignores it in any directive that also contains a nonce or hash. What makes a strict policy hard is usually third-party code that loads further scripts, such as tag managers and chat widgets, so write down the reason for every exception you add.

default-src is the fallback for fetch directives such as script-src, but not for frame-ancestors: a policy of default-src 'none' still lets any site frame the page. Set frame-ancestors 'self' explicitly, or 'none', which is similar to X-Frame-Options: DENY (MDN). The OWASP HTTP Headers Cheat Sheet says frame-ancestors makes X-Frame-Options obsolete in browsers that support it, while X-Frame-Options still covers older browsers; if you send both, make them allow the same framing. Two traps: frame-ancestors, a report-only policy and X-Frame-Options have no effect in a <meta> tag, and X-Frame-Options: ALLOW-FROM makes modern browsers ignore the whole header.

A first draft for a site without third-party scripts is the recommended policy of the OWASP Secure Headers Project:

default-src 'self'; form-action 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'none'; upgrade-insecure-requests

It blocks inline scripts and styles and anything loaded from other hosts, so expect many reports at first; that is why it starts as Report-Only (section 8). Take only the CSP line: the project’s full set also contains values such as Cache-Control: no-store and Clear-Site-Data, which would disable caching and clear the visitor’s cookies and stored data on every response. On JSON API responses, which the browser does not render, CSP does little.

4. HSTS: ramp up max-age and think twice about preload

Strict-Transport-Security tells the browser to use only HTTPS for the host and to upgrade later http:// requests automatically. Browsers ignore the header when it arrives over plain HTTP, so send it on HTTPS responses, not on the HTTP-to-HTTPS redirect. On its own, it cannot protect a visitor’s very first connection.

Plan for the cost: on an HSTS host, visitors cannot click through certificate errors, so an expired certificate locks out returning visitors. Public TLS certificates issued since 15 March 2026 may be valid for at most 200 days (CA/Browser Forum), and Let’s Encrypt no longer emails expiry reminders, so put automated renewal and your own expiry monitoring in place first. The SSL/TLS and security headers guide covers the certificate side.

Raise max-age (in seconds) in stages, as hstspreload.org recommends, and check for breakage at each step: 300 (5 minutes), 604800 (1 week), 2592000 (1 month), then 31536000 (1 year) or 63072000 (2 years, the value OWASP uses). max-age=0 removes the policy, but only when sent over HTTPS, and only for visitors who come back and receive it. Add includeSubDomains once every subdomain works over HTTPS, and have each subdomain send its own header as well.

Preload builds your domain into browsers so that even the first visit uses HTTPS. The submission site, hstspreload.org, now recommends HSTS but not preloading: Chrome and Safari already upgrade HTTP navigations to HTTPS, so preloading adds little, and a removal from the list takes months to reach users. The OWASP HTTP Headers Cheat Sheet still includes preload in its example header, while the OWASP Secure Headers Project’s recommended value leaves it out. Do not add it by default or copy it from a template.

5. Cookie attributes: Secure, HttpOnly and SameSite

Set-Cookie: __Host-session=…; Path=/; Secure; HttpOnly; SameSite=Lax
  • Secure makes the browser send the cookie only over HTTPS (localhost excepted). It does not hide the cookie from JavaScript.
  • HttpOnly stops scripts on the page from reading the cookie (for example through document.cookie), but the browser still sends it with matching requests, so it limits cookie theft through XSS without preventing XSS itself.
  • SameSite decides whether the cookie goes with cross-site requests. With Strict it never does. With Lax it also goes with top-level GET navigations from another site, such as a visitor following a link to yours. With None it always does, and None requires Secure. Browsers differ on the default, so set it explicitly.
  • The __Host- name prefix makes the browser reject the cookie unless it is set with Secure from an HTTPS origin, with Path=/ and no Domain attribute.

Use Lax or Strict for session cookies unless a flow really needs None, for example a payment or sign-in return that posts back from another site. The OWASP Session Management Cheat Sheet treats SameSite as defense in depth against cross-site request forgery (CSRF), not as a replacement for CSRF tokens.

6. Headers to drop: X-XSS-Protection and other leftovers

  • X-XSS-Protection switched on a filter in old browsers that could itself create XSS vulnerabilities (MDN). OWASP says not to set it, or to turn it off with 0; CSP is the replacement.
  • Expect-CT: OWASP says not to use it and, citing Mozilla, to remove it from existing configurations.
  • Public-Key-Pins (HPKP) was removed from Chromium in 2018 and is not supported by any modern browser.
  • Feature-Policy has been replaced by Permissions-Policy.

Server and X-Powered-By name your software, sometimes with version numbers. OWASP suggests removing them or making them uninformative, but its own testing guide calls this security through obscurity, and MDN says keeping software patched matters more. Remove the version numbers (in nginx, server_tokens off; removes the version, not the header) and spend the effort on updates.

7. A baseline to copy for nginx and static hosts

This nginx baseline sets the low-risk headers, starts HSTS and CSP in their test stages and redirects HTTP to HTTPS on the same host. The always parameter adds the headers to error responses too.

server {
    listen 80;
    server_name example.com;
    return 301 https://example.com$request_uri;
}

server {
    listen 443 ssl;
    server_name example.com;
    # ssl_certificate and ssl_certificate_key go here
    server_tokens off;

    add_header Strict-Transport-Security "max-age=300" 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;
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header Cross-Origin-Opener-Policy "same-origin" always;
    add_header Content-Security-Policy-Report-Only "default-src 'self'; form-action 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'self'" always;
}

Two documented behaviors of the nginx headers module explain many missing headers. Without always, add_header applies only to status codes 200, 201, 204, 206, 301, 302, 303, 304, 307 and 308, so 404 and 500 pages go out without the headers. And add_header directives are inherited from the enclosing level only if the current level defines none: a single add_header inside a location block drops every server-level security header there. Repeat the headers, include a shared file, or, from nginx 1.29.3, use add_header_inherit merge;.

Static hosts such as Cloudflare Pages and Netlify read a _headers file deployed with the site. This example follows Cloudflare’s documented format, which Netlify uses as well:

/*
  X-Content-Type-Options: nosniff
  Referrer-Policy: strict-origin-when-cross-origin
  Permissions-Policy: camera=(), microphone=(), geolocation=()
  X-Frame-Options: SAMEORIGIN
  Content-Security-Policy-Report-Only: default-src 'self'; form-action 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'self'

Know the limits. Cloudflare Pages does not apply the file to responses generated by Pages Functions, allows up to 100 header rules with at most 2,000 characters per line, and joins the values with a comma when the same header is applied twice. Netlify applies custom headers only to files it serves itself, not to proxied content, functions, edge functions or server-rendered pages; those responses must set their own headers. HSTS is left out of the example: check with curl what the platform already sends before you add it.

8. Roll out in stages and avoid the common mistakes

  1. Add nosniff, Referrer-Policy, Permissions-Policy and framing protection, then test sign-in, checkout and embedded widgets.
  2. Send the draft CSP as Content-Security-Policy-Report-Only. Nothing is blocked; violations appear in the browser console, and at a reporting endpoint if the policy names one. Walk through the key journeys, including pages behind sign-in, and adjust the policy.
  3. Start HSTS with a short max-age and raise it in stages.
  4. Enforce the CSP. A stricter Report-Only policy can run alongside it to test the next tightening.
  5. Re-run the curl checks after every server, CDN or framework change.

Common mistakes:

  • Headers on some responses only. The home page has them; the 404 page, the API, static files or a separately served login page do not.
  • Changing host and scheme in one redirect. If http://example.com redirects straight to https://www.example.com, the browser never receives HSTS for example.com. Redirect to HTTPS on the same host first, send HSTS on that HTTPS response, and only then redirect to www.
  • The CDN overrides the origin. A CDN or proxy can add, replace, strip or duplicate headers. Decide which layer owns each header and confirm the result with curl.
  • A CSP that allows everything. A policy padded with 'unsafe-inline', 'unsafe-eval' or * to silence errors passes a presence check but blocks little.
  • includeSubDomains too early. A forgotten subdomain that only works over HTTP becomes unreachable for browsers that have stored the policy.

Headers are one layer. The website launch checklist puts them next to DNS, TLS and redirects, and the website security audit guide covers sign-in, permissions and a repeatable review routine.

9. How the free Launch Readiness Snapshot checks headers

The free Launch Readiness Snapshot runs eight passive checks on one public origin without signup; one of them reads HTTP security headers. It is not a pentest and not the six-area audit.

  • It requests the home page of the origin you enter (paths and query strings are dropped), follows up to five redirects and judges the final response. No other pages are requested.
  • It mostly checks presence. A missing CSP, missing HSTS on an HTTPS target, no framing protection and Access-Control-Allow-Origin: * are rated medium. X-Content-Type-Options other than nosniff, a missing Referrer-Policy and any Server or X-Powered-By header are rated low, so Server: nginx without a version still raises a low signal. Cookies set on that response without HttpOnly, or without Secure on HTTPS, are rated medium; SameSite is not checked.
  • A separate hardening grade (A to F) scores the headers present together with the TLS result, and gives CSP less credit when it allows 'unsafe-inline' (or 'unsafe-eval' for scripts). Permissions-Policy and COOP count only in this grade.
  • The snapshot’s separate HTTPS redirect check depends on what you enter. For a bare domain or an https:// address, it flags an HSTS max-age under 180 days as low, which is expected during a ramp-up, and looks for http:// references in the page HTML (mixed content); it does not test the HTTP-to-HTTPS redirect. The redirect is tested only when you enter the http:// address, and that run skips the TLS check and does not report missing HSTS.

The result shows one score, the hardening grade, the number of risk signals and passed checks, and up to three signals, risks first. It does not list every header and its value, so use DevTools or curl for that and for the responses the snapshot does not request.

Common questions

How do I check the security headers of my website?

In browser DevTools, open the Network panel, reload the page, select the HTML document and read Response Headers. In a terminal, curl -sS -D - -o /dev/null https://example.com/ prints them. Check the http:// address, a 404 page, an API response and a static file too, because their headers often differ.

Which HTTP security headers should I set first?

Start with X-Content-Type-Options: nosniff, Referrer-Policy, Permissions-Policy and framing protection, which are the least likely to break anything; still test scripts, sign-in and embedded widgets afterwards. Then test a Content-Security-Policy in Report-Only mode and raise HSTS max-age in stages.

Should I still send X-XSS-Protection?

No. Remove it or send X-XSS-Protection: 0, and rely on a Content-Security-Policy instead. The old browser filter it controlled could itself create XSS vulnerabilities.

Do I need X-Frame-Options if I already use frame-ancestors?

The CSP frame-ancestors directive replaces it in browsers that support it. X-Frame-Options: DENY or SAMEORIGIN is still a reasonable fallback for older browsers, as long as both headers allow the same framing. Do not use ALLOW-FROM: modern browsers ignore the whole header when they see it.

Should I add my domain to the HSTS preload list?

Usually not. The preload submission site, hstspreload.org, now recommends HSTS but not preloading, because Chrome and Safari already upgrade HTTP navigations to HTTPS. A removal from the list also takes months to reach users.

Do security headers make my site secure?

Not on their own. They limit the damage from injected scripts, clickjacking, protocol downgrades and cookie theft. They do not fix vulnerable code, outdated software or weak access control.

Sources & further reading

  1. MDN: Content Security Policy (CSP) guidedeveloper.mozilla.org
  2. MDN: CSP frame-ancestors directivedeveloper.mozilla.org
  3. MDN: Strict-Transport-Security headerdeveloper.mozilla.org
  4. MDN: Set-Cookie header and cookie attributesdeveloper.mozilla.org
  5. OWASP HTTP Headers Cheat Sheetcheatsheetseries.owasp.org
  6. OWASP Secure Headers Project: recommended header valuesraw.githubusercontent.com
  7. HSTS Preload List Submissionhstspreload.org
  8. nginx: ngx_http_headers_module (add_header)nginx.org
  9. Cloudflare Pages: Headersdevelopers.cloudflare.com
  10. Netlify: Custom headersdocs.netlify.com
Sitelemetry team

Prepared by the Sitelemetry editorial team. Explore the linked sources for further detail.

SITELEMETRY

Put what you learned to work.

Explore your website’s structure, settings and signals with Sitelemetry.