Guide

WordPress security headers: what to add, and what can break your site

A security scan says your site is missing headers: HSTS, Content-Security-Policy, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy. They are lines your server sends with every page, telling the browser how strictly to treat it. WordPress itself sends almost none of them on the pages visitors see. Three can be added today with no real risk. The other three need a short plan, because a wrong value can lock visitors out of the site or block your own scripts. This guide covers each one, where to set it on WordPress, and why headers that were there last month go missing again.

What security headers are, and why a missing one matters

Every response from your server starts with headers: the content type, caching rules, cookies. Security headers are a handful more that switch on protections the browser already has but won't use unless the site asks. X-Frame-Options stops another site from loading yours in a hidden frame and tricking visitors into clicking (clickjacking). HSTS tells the browser to use HTTPS only, even when someone types the address without it. Content-Security-Policy lists where scripts may load from, which limits what injected code can do.

A missing header isn't a hole in itself, and it doesn't mean the site was hacked. It means one of the browser's defences is off. That's why scanners report them as warnings rather than failures — and why it's worth fixing the safe ones and planning the rest, not ignoring the whole list.

WordPress core sends X-Frame-Options: SAMEORIGIN (and a matching frame-ancestors rule) on the login page and in wp-admin. The front of the site — the pages a scan looks at — gets none of the six unless your host, your CDN or a plugin adds them.

How to see which ones your site sends

In the browser: open the homepage, then Developer tools → Network, reload, click the first request (the page itself) and read "Response headers". From a terminal, the command below follows redirects the way a browser does and prints only the six security headers. Whatever doesn't appear in the output is missing.

Check the final page after redirects, over HTTPS. A header that the http:// address sends but the https:// page doesn't still counts as missing for visitors.

curl -sIL https://example.com/ | grep -iE "strict-transport-security|content-security-policy|x-frame-options|x-content-type-options|referrer-policy|permissions-policy"

The three you can add today

X-Content-Type-Options: nosniff stops the browser from guessing a file's type: a file served as text isn't run as a script, and a script or stylesheet with the wrong content type is blocked. On a normal WordPress site nothing changes, because WordPress, the web server and well-built plugins already send correct types.

X-Frame-Options: SAMEORIGIN lets your own pages frame each other but stops every other site from framing yours. The one thing it breaks is a page you deliberately embed on another domain — a booking form inside a partner's site, say. If you do that, use Content-Security-Policy's frame-ancestors directive instead, which accepts a list of allowed sites.

Referrer-Policy: strict-origin-when-cross-origin sends other sites only your domain, not the full address of the page a visitor came from, and nothing at all from HTTPS to HTTP. It's also what current browsers do when no policy is set, so adding it mainly makes that behaviour explicit. Of the six, it's the lowest risk to leave missing and the easiest to add.

On Apache (most shared WordPress hosting), put these lines in .htaccess above the # BEGIN WordPress block — WordPress rewrites only what's between its own markers, so the lines stay when permalinks are saved. The Permissions-Policy line is explained further down; leave it out until you've read that part.

<IfModule mod_headers.c>
  Header set X-Content-Type-Options "nosniff"
  Header set X-Frame-Options "SAMEORIGIN"
  Header set Referrer-Policy "strict-origin-when-cross-origin"
  Header set Permissions-Policy "camera=(), microphone=(), geolocation=()"
</IfModule>

HSTS: only once HTTPS works everywhere

Strict-Transport-Security makes the browser remember, for max-age seconds, that the site is HTTPS-only. It stops the first plain-HTTP request — the one an attacker on public Wi-Fi can intercept — and turns certificate warnings into hard errors visitors can't click past. Browsers ignore the header when it arrives over plain HTTP, so it only works from an HTTPS page.

The risk is the memory. If the certificate lapses or a subdomain still runs on HTTP, browsers that have seen the header refuse to load it until max-age runs out, and you can only switch it off (max-age=0) for browsers that reach the site over HTTPS again. So roll it out in steps: start with max-age=300 (five minutes), check every page and subdomain, then raise it to a day, a week, and finally 31536000 (a year). Add includeSubDomains only when every subdomain — mail, shop, staging — serves HTTPS.

The preload directive is a separate decision. It asks browsers to ship your domain in their built-in HTTPS-only list; it needs a year's max-age plus includeSubDomains, and getting off the list again takes a long time. Leave it out unless you're sure. If the site is behind Cloudflare, HSTS is a setting under SSL/TLS → Edge Certificates rather than a server line.

Content-Security-Policy: start in report-only

A full Content-Security-Policy lists every origin that may load scripts, styles, fonts, images and frames on the site. It's the strongest of the six against injected code, and the one most likely to break a WordPress site: themes, page builders, analytics, consent banners, chat widgets and payment buttons all load scripts from somewhere, often inline. Copying a strict policy from a tutorial will block some of them.

Send it as Content-Security-Policy-Report-Only first. The browser enforces nothing and logs every violation in the console, so you can browse the site — the checkout too — and add each origin you actually need. Switch the header's name to Content-Security-Policy only when the console stays quiet.

A short policy that rarely breaks anything is a reasonable first enforced step while you work on the full one: object-src 'none'; base-uri 'self'; frame-ancestors 'self'. It blocks old plugin embeds, stops injected code from changing where relative links point, and does the same job as X-Frame-Options: SAMEORIGIN.

Set it as a header. A policy in a <meta http-equiv> tag works for most directives, but frame-ancestors is ignored there, and tools that read response headers won't see it.

Permissions-Policy: switch off what you don't use

Permissions-Policy decides which browser features — camera, microphone, location and others — the page and anything it embeds may ask for. camera=(), microphone=(), geolocation=() switches those three off everywhere on the site, so a script injected into a page or an embedded ad can't ask visitors for them.

List only features you know the site doesn't use. A store locator that asks for the visitor's location needs geolocation=(self) instead of geolocation=(); a video-call or recording plugin needs camera and microphone. Check the pages that use such features after adding it.

Where to set them on WordPress

There are three places, and the higher up, the more they cover. The web server (.htaccess on Apache, the server block on Nginx) covers every response, including images, scripts and cached pages. A CDN in front of the site covers everything that passes through it. Inside WordPress — a security plugin, a few lines on the send_headers hook, or Relvato's "Add safe security headers" — covers only the pages WordPress itself builds; a page served by a cache before WordPress fully loads may go out without them. That's fine for a quick fix, but check the result on the live homepage.

On Nginx, use add_header with always, so the header is also sent on error pages. Watch for one trap: a location block that has any add_header of its own doesn't inherit the ones from the server block. Someone adds a caching header to one location and the security headers quietly disappear from it.

Set each header in one place. Two layers adding the same header send it twice, and two Content-Security-Policy headers are both enforced, so the effective policy is stricter than either one.

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;

Why headers go missing again

Headers rarely get removed on purpose. They disappear when the place that set them changes: a move to a new host (Nginx doesn't read .htaccess at all, so every rule in it stops working), a new CDN or a reset CDN setting, a caching plugin swapped for another that serves pages before WordPress loads, a security plugin deactivated or its option reset by an update, or a new Nginx location block that stops inheriting them.

None of these produce an error. The site looks the same, so the only way to notice is to check the headers again after each change — which is exactly the kind of thing nobody does by hand after every update.

How Relvato catches it

Relvato's Exposure scan monitor fetches your homepage the way a browser does, following redirects, and reads the response headers. It works on any site, not just WordPress. Each missing header is its own warning — "Missing security header: HSTS", "Missing security header: Content-Security-Policy" and so on — on a run that otherwise passes. Because each is separate, you can press "Ignore this warning" on one you've decided against for now, such as CSP, and still hear about the others. It reads headers only, so a policy in a meta tag doesn't count.

On WordPress with the Relvato plugin, the three safe headers get "Fix in WordPress" next to the warning. In WordPress → Relvato, one click turns on "Add safe security headers": X-Content-Type-Options: nosniff, Referrer-Policy: strict-origin-when-cross-origin and X-Frame-Options: SAMEORIGIN, each only if the site doesn't send it already. "Switch off" turns it back off there. HSTS, Content-Security-Policy and Permissions-Policy are never added for you, because a wrong value breaks sites — see what Relvato won't fix for you.

On the Free plan you press Run whenever you want the scan, or schedule it weekly. Pro re-runs your monitors by itself after every update, so a header that a plugin update or a host change took away shows up as a warning again without anyone remembering to look.

The six security headers at a glance

HeaderWhat it doesA safe value to start withCan it break something?
X-Content-Type-OptionsStops the browser guessing file typesnosniffAlmost never on WordPress
X-Frame-OptionsStops other sites framing yours (clickjacking)SAMEORIGINOnly pages you embed on other domains
Referrer-PolicyLimits what other sites learn about the page a visitor came fromstrict-origin-when-cross-originNo — it's the browser default
Permissions-PolicySwitches off camera, microphone, location and other featurescamera=(), microphone=(), geolocation=()Yes, if a page uses one of those features
Strict-Transport-Security (HSTS)Makes browsers use HTTPS onlymax-age=300, raised step by stepYes — locks out HTTP subdomains and lapsed certificates
Content-Security-PolicyLists where scripts, styles and frames may load fromReport-Only first; object-src 'none'; base-uri 'self'; frame-ancestors 'self'Yes — the most likely to block your own scripts

Questions, answered

Which security header should I add first?

X-Content-Type-Options, X-Frame-Options and Referrer-Policy, together: they are safe on almost every WordPress site and take a few lines. Then Permissions-Policy for features you don't use, then HSTS in steps, and Content-Security-Policy last, in report-only mode first.

Do security headers help SEO?

Google doesn't list security headers as a ranking signal; HTTPS is one. HSTS helps by keeping visitors on HTTPS. The real reason to add headers is protecting visitors, not rankings.

Can a plugin add them for me?

Yes, and several security plugins do. A plugin adds them only to pages WordPress builds, so check the live homepage afterwards — a page cache or CDN may serve pages without them. Set each header in one place only, so it isn't sent twice.

I added a CSP meta tag. Why is the header still reported missing?

The scan reads the response headers, and a <meta http-equiv> policy isn't one. Browsers apply most directives from the meta tag, but not frame-ancestors. Sending the policy as a header covers everything.

Does a missing security header mean my site was hacked?

No. It means a browser protection is switched off. If you're worried about a break-in, the signs look different — see the WordPress hacked checklist.

Sources

Related reading
Guide

Know the day a header goes missing.

Relvato's Exposure scan checks your headers with the rest of what a site gives away, and on WordPress suggests the safe ones, added with one click in WordPress → Relvato — with Undo.