How to add security headers to your site

Part of the WEBCHKR fix guides · Updated 2026-10-18

Security headers are HTTP response headers that tell browsers how to treat your pages: always use HTTPS, do not let other sites load you in a frame, do not guess file types, limit what goes in the Referer header, and switch off browser features the site does not use. They are set on your server or CDN, not in page content, and most sites can add the core set in a few minutes.

The headers and safe starting values

The first three are the core set. X-Content-Type-Options and X-Frame-Options are safe on almost any site; add HSTS once you have confirmed that every subdomain it covers works over HTTPS. The other three are worth adding once those are in place; Content-Security-Policy takes the most care.

HeaderStarting valueWhat it does
Strict-Transport-Securitymax-age=31536000; includeSubDomainsAfter one visit over HTTPS, browsers go straight to HTTPS for a year without trying http first. Add it only once every subdomain works over HTTPS.
X-Content-Type-OptionsnosniffStops browsers guessing a file's type, which blocks some script-injection tricks.
X-Frame-OptionsSAMEORIGINStops other sites loading yours in a frame (clickjacking). frame-ancestors 'self' in a Content-Security-Policy does the same in modern browsers.
Referrer-Policystrict-origin-when-cross-originSends only your site's address (such as https://example.com/), not the full page URL, when your pages link to or load from other sites, and nothing to plain-http sites. Browsers already do this by default; the header makes it explicit.
Permissions-Policycamera=(), microphone=(), geolocation=()Switches off browser features the site does not use, for your pages and anything they embed. Chrome and Edge honor it; Firefox and Safari currently ignore it.
Content-Security-PolicySpecific to your siteLists where scripts, styles, images and frames may load from. Done strictly, it is the strongest protection against injected scripts, and it takes testing.

Add them on your platform

Cloudflare Pages and Netlify

Create a file named _headers in the root of the site's output. It covers static files; pages generated by Functions or server-side rendering need the headers set in that code instead.

/*
  Strict-Transport-Security: max-age=31536000; includeSubDomains
  X-Content-Type-Options: nosniff
  X-Frame-Options: SAMEORIGIN
  Referrer-Policy: strict-origin-when-cross-origin
  Permissions-Policy: camera=(), microphone=(), geolocation=()

Any site behind Cloudflare

Under Rules, create a response header transform rule that sets each header on all requests. HSTS has its own setting under SSL/TLS → Edge Certificates.

Vercel

{
  "headers": [
    {
      "source": "/(.*)",
      "headers": [
        { "key": "Strict-Transport-Security", "value": "max-age=31536000; includeSubDomains" },
        { "key": "X-Content-Type-Options", "value": "nosniff" },
        { "key": "X-Frame-Options", "value": "SAMEORIGIN" },
        { "key": "Referrer-Policy", "value": "strict-origin-when-cross-origin" },
        { "key": "Permissions-Policy", "value": "camera=(), microphone=(), geolocation=()" }
      ]
    }
  ]
}

That goes in vercel.json. Vercel already sends HSTS with a two-year max-age, so leave that line out unless you want includeSubDomains. In Next.js, the same list can live in the headers() function in next.config.js.

Apache

<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 X-Frame-Options "SAMEORIGIN"
  Header always set Referrer-Policy "strict-origin-when-cross-origin"
  Header always set Permissions-Policy "camera=(), microphone=(), geolocation=()"
</IfModule>

Nginx

add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;

Put these in the server block. By default, any add_header inside a location block replaces all of the inherited ones for that location, so repeat them there or use an include file. Nginx 1.29.3 and later can merge them instead with add_header_inherit merge;.

WordPress and hosted site builders

On WordPress, add the headers at the server (the Apache or Nginx snippets above), through your host's settings, or with a security plugin that sets headers. Hosted builders such as Shopify, Squarespace and Wix set their own headers and do not let you change most of them (Wix allows it only on its enterprise plan); focus on the settings you do control.

Set each header in one place

If your CDN and your server both add the same header, it can arrive twice. Browsers cope with an exact repeat such as X-Frame-Options: SAMEORIGIN, SAMEORIGIN, but different values clash: SAMEORIGIN, DENY blocks all framing, a second Content-Security-Policy adds its limits to the first, and a doubled HSTS value can be ignored. Some checkers flag an exact repeat as invalid; WEBCHKR notes it but, like browsers, accepts it. Pick one layer for each header and remove it from the other.

Content-Security-Policy without breaking the site

A CSP that misses a source blocks it, so a chat widget, analytics tag or embedded video stops working. Roll it out in report-only mode first:

Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self' https://www.googletagmanager.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; frame-ancestors 'self'
  1. List the outside services the site loads: analytics, tag manager, fonts, chat, video, payment and booking widgets.
  2. Publish the policy as Content-Security-Policy-Report-Only. Browsers report what it would block in the developer tools console but block nothing.
  3. Browse every page type, add each legitimate source to the right directive, and repeat until the console is clean.
  4. Rename the header to Content-Security-Policy.

Browsers ignore upgrade-insecure-requests in a report-only policy, so if you want it, add it once the policy is enforced. Avoid * wildcards, a bare https: in script-src (the https: in img-src above is a deliberate allowance for images), and 'unsafe-eval'.

Tag managers make a strict policy hard. Google Tag Manager's standard snippet is an inline script, so the example above blocks it until you add a nonce or hash to script-src (Google's guide to CSP for Tag Manager shows how), and Google Analytics also needs entries in connect-src. Allow the sources you actually use and revisit the policy when you add a new tag.

HSTS preload, carefully

Preloading builds HSTS into browsers themselves, so even the first visit uses HTTPS. To qualify, the domain needs a valid certificate, a redirect from http to https on the same host before any redirect to www, HTTPS on every subdomain including www, and Strict-Transport-Security with a max-age of at least 31536000, includeSubDomains and preload on its HTTPS responses, including redirects. hstspreload.org checks all of this before you submit. Only do it if every subdomain, now and in future, will serve HTTPS: removal takes months to reach Chrome users, and other browsers may take longer.

Check it worked

Run WEBCHKR again. The Security Headers check reads the headers on your home page over HTTPS. It passes when the three core protections are present: HSTS with a max-age above zero, X-Content-Type-Options: nosniff, and clickjacking protection from either X-Frame-Options (DENY or SAMEORIGIN) or a CSP frame-ancestors that does not allow any site (such as * or https:). It warns with two of the three and fails with fewer. A header sent twice with the same value is accepted, as browsers accept it, and noted so you can tidy it up. Content-Security-Policy, Referrer-Policy and Permissions-Policy are listed as recommended, and an HSTS max-age under one year is flagged.