Security headers checker

A security headers checker reads the HTTP response headers of a page and compares them with recommended values for HSTS, Content-Security-Policy, framing, content type sniffing, referrer policy and permissions policy. This free checker makes one GET request per redirect, reads headers only and stores nothing. It is not a penetration test.

Enter a public address to check its security headers, then use the fix for each result below.

One GET request, like a browser visit. We read the response headers only and do not store the address or the page.

What the checker does and does not do#

The checker sends one GET request per redirect, like a browser visit, and reads the response headers. It does not read or store the page body. It does not log in, crawl, fuzz or send attack payloads.

  • It follows up to 5 redirects and checks each hop.
  • It accepts public http and https addresses on the default ports. It refuses IP addresses, internal names and names that resolve to private addresses.
  • It grades each check as pass, warn, fail or info.
  • It is not a penetration test. It cannot find broken access control, injection or logic flaws. See when headers are not enough.

Use it only on sites you own or have permission to test. A header check is a normal page request, but you should still ask first for a site that is not yours.

What each check looks for and the value to set#

Each section gives the value we recommend, the reason, and what the checker reports. Values follow the OWASP HTTP Headers Cheat Sheet unless noted.

HTTPS#

The final page must load over https. The checker passes an http address that redirects to https. It fails a final page served over plain http, because anyone on the network can then read or change it.

Strict-Transport-Security (HSTS)#

Set Strict-Transport-Security: max-age=63072000; includeSubDomains. HSTS tells the browser to use https for your site even if a user types http (RFC 6797). Without it, the first request can still go out over http, which allows a downgrade.

The checker passes a max-age of at least 31536000 seconds (one year). It warns on a shorter value and fails when the header is missing or max-age is zero. Browsers ignore the header over plain http, so it only counts on https responses.

Content-Security-Policy (CSP)#

Set a policy that limits where scripts load from. A starting point for a site with no inline scripts is default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'. CSP reduces the impact of cross-site scripting (W3C CSP Level 3).

The checker fails when no policy exists and warns when only Content-Security-Policy-Report-Only is set, because that mode reports violations but blocks nothing. It warns on 'unsafe-inline' without a nonce or hash, 'unsafe-eval', wildcard or scheme sources in script-src, no script-src or default-src, and no object-src.

Clickjacking protection#

Use frame-ancestors in your CSP, or X-Frame-Options: DENY (or SAMEORIGIN if you frame your own pages). OWASP prefers frame-ancestors. This stops other sites from loading your page in a frame and tricking users into clicking. The checker passes either control and fails when neither is set.

X-Content-Type-Options#

Set X-Content-Type-Options: nosniff. It stops browsers from guessing a file's type, which can turn an uploaded file into a script. Pair it with correct Content-Type headers. The checker passes only the exact value nosniff.

Referrer-Policy#

Set Referrer-Policy: strict-origin-when-cross-origin. It keeps full URLs, including query strings, from reaching other sites. Modern browsers use this value by default, and OWASP advises setting it explicitly. The checker passes no-referrer, same-origin, strict-origin and strict-origin-when-cross-origin, fails unsafe-url, and warns on other values or a missing header.

Permissions-Policy#

Set Permissions-Policy: geolocation=(), camera=(), microphone=(). List every browser feature your site does not use. An injected script then cannot switch them on. The checker passes when the header is present and warns when it is missing.

Cross-Origin-Opener-Policy#

Cross-Origin-Opener-Policy: same-origin isolates your page from windows it opens or that open it. The checker reports this as info, not a failure. Test sign-in pop-ups and payment windows before you add it, because they can break.

Version disclosure#

Remove X-Powered-By, X-AspNet-Version, X-Generator and any version number in Server. OWASP suggests removing them or using a neutral value. This is a small gain, because attackers can fingerprint a stack in other ways. The checker warns when it finds one.

Session cookies need Secure, HttpOnly and an explicit SameSite. The checker reads the cookies set on the response and warns if any lacks Secure on https or an explicit SameSite. It reports at most 20 cookies.

Other items you may see#

  • X-XSS-Protection: shown as info when set to anything but 0. OWASP says to set 0 or omit it.
  • CORS: shown when Access-Control-Allow-Origin is present. A warning appears when credentials are allowed cross-origin. Use a fixed list of origins and never reflect the request origin.

Copy-paste config by stack#

These blocks set the same baseline. Replace the CSP with one your site can run, and test it before release. Start with Content-Security-Policy-Report-Only if you are unsure.

Stack Where it goes
nginx server block
Apache virtual host or .htaccess (needs mod_headers)
Caddy site block in the Caddyfile
Cloudflare Worker, or a Transform Rule (Modify Response Header)
Next.js next.config.js
Express helmet middleware
Vercel vercel.json
Netlify _headers file

nginx. Add always so headers also appear on error pages.

add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), camera=(), microphone=()" always;
server_tokens off;

nginx only inherits add_header lines from a parent level when the current level has none (nginx docs). One add_header inside a location block drops all the parent headers for that location.

Apache (mod_headers).

Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains"
Header always set Content-Security-Policy "default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'"
Header always set X-Content-Type-Options "nosniff"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Permissions-Policy "geolocation=(), camera=(), microphone=()"
Header always unset X-Powered-By
ServerTokens Prod

Caddy.

header {
    Strict-Transport-Security "max-age=63072000; includeSubDomains"
    Content-Security-Policy "default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'"
    X-Content-Type-Options "nosniff"
    Referrer-Policy "strict-origin-when-cross-origin"
    Permissions-Policy "geolocation=(), camera=(), microphone=()"
    -Server
    -X-Powered-By
}

Cloudflare Worker. A Transform Rule with "Modify Response Header" sets the same values without code. This Worker does it in code.

export default {
  async fetch(request, env, ctx) {
    const res = await fetch(request);
    const out = new Response(res.body, res);
    out.headers.set("Strict-Transport-Security", "max-age=63072000; includeSubDomains");
    out.headers.set("Content-Security-Policy", "default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'");
    out.headers.set("X-Content-Type-Options", "nosniff");
    out.headers.set("Referrer-Policy", "strict-origin-when-cross-origin");
    out.headers.set("Permissions-Policy", "geolocation=(), camera=(), microphone=()");
    out.headers.delete("X-Powered-By");
    return out;
  },
};

Next.js (next.config.js).

module.exports = {
  async headers() {
    return [
      {
        source: "/(.*)",
        headers: [
          { key: "Strict-Transport-Security", value: "max-age=63072000; includeSubDomains" },
          { key: "Content-Security-Policy", value: "default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'" },
          { key: "X-Content-Type-Options", value: "nosniff" },
          { key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
          { key: "Permissions-Policy", value: "geolocation=(), camera=(), microphone=()" },
        ],
      },
    ];
  },
  poweredByHeader: false,
};

Next.js injects inline scripts, so a strict script-src 'self' will block them. Use a per-request nonce, as the Next.js documentation describes, or start in Report-Only mode.

Express with helmet.

import helmet from "helmet";

app.disable("x-powered-by");
app.use(
  helmet({
    strictTransportSecurity: { maxAge: 63072000, includeSubDomains: true },
    contentSecurityPolicy: {
      directives: {
        defaultSrc: ["'self'"],
        scriptSrc: ["'self'"],
        objectSrc: ["'none'"],
        baseUri: ["'self'"],
        frameAncestors: ["'self'"],
      },
    },
    referrerPolicy: { policy: "strict-origin-when-cross-origin" },
  })
);

Helmet sets X-Content-Type-Options: nosniff by default. Add Permissions-Policy with a small middleware, because helmet does not set it.

Vercel (vercel.json).

{
  "headers": [
    {
      "source": "/(.*)",
      "headers": [
        { "key": "Strict-Transport-Security", "value": "max-age=63072000; includeSubDomains" },
        { "key": "Content-Security-Policy", "value": "default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'" },
        { "key": "X-Content-Type-Options", "value": "nosniff" },
        { "key": "Referrer-Policy", "value": "strict-origin-when-cross-origin" },
        { "key": "Permissions-Policy", "value": "geolocation=(), camera=(), microphone=()" }
      ]
    }
  ]
}

Netlify (_headers file in the publish directory).

/*
  Strict-Transport-Security: max-age=63072000; includeSubDomains
  Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'
  X-Content-Type-Options: nosniff
  Referrer-Policy: strict-origin-when-cross-origin
  Permissions-Policy: geolocation=(), camera=(), microphone=()

Common mistakes#

CSP with 'unsafe-inline'. It allows any inline script, so an injected script runs. That removes most of the value of the policy. Move scripts to files, or use a nonce or hash for each inline script. The checker does not warn on 'unsafe-inline' when a nonce, hash or 'strict-dynamic' is also present.

Adding preload too early. The preload list is built into browsers. hstspreload.org requires max-age of at least 31536000, includeSubDomains and preload, and https on every subdomain, including internal ones. It states that removal takes months to reach users. Test HSTS without preload first.

Duplicate headers from proxy and app. A CDN, load balancer and app can each add the same header with different values. Browsers may combine or pick one, depending on the header. Set each header in one layer. Compare the checker output with a request straight to the app to find the extra layer.

Headers missing on errors and redirects. In nginx, add always. In Apache, use Header always. Many scanners and attackers see 404 and 301 responses first.

Setting CSP and breaking the site. Deploy Content-Security-Policy-Report-Only first, read the reports, fix the sources, then switch to enforcement.

When headers are not enough#

Headers harden the browser side of a site. They do not show whether an attacker can read another user's records, skip a payment step or take over an account. Those flaws sit in application logic, and only testing finds them.

If you want that testing, read how web application penetration testing works. For related reading, see penetration testing tools, DAST and vulnerability assessment. Before a launch, see pre-launch security testing. To collect what providers ask before they quote, use the free pentest scoping checklist from PentestCompare.

Questions

Does a good grade mean my site is secure?

No. Headers reduce the impact of some browser-side attacks. They do not test login, access control, injection or business logic. A site can pass every header check and still let one customer read another customer's data.

Do I need a Content-Security-Policy on a JSON API?

It matters little there. OWASP notes that CSP is aimed at pages that run scripts and may be pointless on REST responses that a browser never renders. Set nosniff and remove version headers on APIs, and put CSP on your HTML pages.

Should I submit my domain to the HSTS preload list?

Only if every subdomain will serve HTTPS for the long term. hstspreload.org says removal takes months to reach users and that preloading is not recommended in general. Run HSTS with includeSubDomains for a while first.

Why does the checker show a header my app does not set?

A proxy, CDN or host can add headers between your app and the browser. The checker sees the final response, which is what a browser sees. Compare it with your app's own output to find who sets what.

Is X-XSS-Protection still needed?

No. OWASP recommends setting it to 0 or leaving it out, because the old browser filter could create XSS in safe pages. Use a Content-Security-Policy that blocks inline scripts.

Sources

  1. OWASP Secure Headers Project (checked 9 November 2026)
  2. OWASP HTTP Headers Cheat Sheet (checked 9 November 2026)
  3. RFC 6797: HTTP Strict Transport Security (HSTS) (checked 9 November 2026)
  4. W3C Content Security Policy Level 3 (checked 9 November 2026)
  5. MDN: Strict-Transport-Security header (checked 9 November 2026)
  6. MDN: Content-Security-Policy header (checked 9 November 2026)
  7. MDN: X-Frame-Options header (checked 9 November 2026)
  8. MDN: X-Content-Type-Options header (checked 9 November 2026)
  9. MDN: Referrer-Policy header (checked 9 November 2026)
  10. MDN: Permissions-Policy header (checked 9 November 2026)
  11. MDN: Cross-Origin-Opener-Policy header (checked 9 November 2026)
  12. HSTS Preload List submission requirements (checked 9 November 2026)
  13. nginx: ngx_http_headers_module (add_header) (checked 9 November 2026)

Need a pentest you can act on?

Tell us what you need tested and why. We reply to agree scope, and testing starts only after written authorization.