WordPress Security Headers: Two You Can Set Today, One That Needs An Afternoon.

We read the headers on four real WordPress sites. Two of them send no nosniff at all, and one sends HSTS with a one hour lifetime.

Aditya Sharma·Updated ·9 min read

Short answer: two wordpress security headers carry real benefit and cannot break anything, so set them now on every site: X-Content-Type-Options: nosniff and a Referrer-Policy. The rest range from mild risk to genuinely disruptive, and a Content Security Policy deserves a proper rollout rather than a quick paste.

I read the headers on four real sites in this industry rather than working from a checklist, and later went back to read them all again to see what, if anything, had changed since.

What real sites actually send

Matrix of six security headers across sitea.example, woocommerce.com, siteb.example and wordpress.org. sitea.example sends all six. woocommerce.com sends four plus a minimal CSP. siteb.example sends three. wordpress.org sends X-Frame-Options and a one hour HSTS only
Requested with a normal browser user agent on 7 September 2026. Dashes are headers not

sent at all. Two of these four sites send no nosniff, which is the one header on the list with no downside whatsoever.*

Headersitea.examplewoocommerce.comsiteb.examplewordpress.org
strict-transport-security1 year1 year, +subdomains1 year1 hour
x-content-type-optionsnosniffnosniffnosniffnot sent
x-frame-optionsSAMEORIGINSAMEORIGINSAMEORIGINSAMEORIGIN
referrer-policystrict-origin-wcostrict-origin-wconot sentnot sent
content-security-policyreal policyupgrade-insecure onlynot sentnot sent
permissions-policycamera/mic/geo offnot sentnot sentnot sent

Caveat: four sites, ours and three reference points, read once. This is not a claim about WordPress sites generally. It is enough to show that even well-run, high-traffic sites vary widely on this, which is the actual point: check yours rather than assume the industry has it.

Two details in there are worth pulling out.

wordpress.org sends HSTS with max-age=3600. That is one hour. The value needed for preload eligibility is a year, and an hour is short enough that a visitor who has not returned within it gets no protection at all on their next first request.

woocommerce.com sends a CSP containing only upgrade-insecure-requests. That is a useful directive, but it is not a content security policy in any meaningful sense. It rewrites HTTP subresource requests to HTTPS. It does not restrict what may execute.

Neither of those is negligence. They are the ordinary, unremarkable state of security headers on real, actively maintained sites, which is exactly why reading them yourself is more useful than assuming.

Which WordPress security headers earn their risk

Table of six headers ranked by risk of breakage against what each prevents. nosniff and referrer-policy carry no risk. X-Frame-Options and permissions-policy carry low risk. HSTS is medium and hard to undo. CSP is high risk and the most valuable
Ordered by value against the chance of breaking something. The top two cannot break a

working site. The bottom one is the most valuable and by far the most likely to take a WordPress site down on first attempt.*

HeaderBreakage riskWhat it stops
x-content-type-options: nosniffnonea browser guessing an uploaded .txt is JavaScript and running it
referrer-policynoneleaking full URLs, including tokens, to every site you link to
x-frame-options / frame-ancestorslowclickjacking, check legitimate embeds first
permissions-policylowa compromised script requesting camera, mic or location
strict-transport-securitymediumdowngrade attacks; hard to undo once browsers remember it
content-security-policyhighinjected scripts executing, the most valuable and riskiest

The HSTS row deserves a warning. Browsers remember max-age, so if you set a year and then discover a subdomain that cannot do HTTPS, you cannot simply remove the header. Every browser that saw it will keep enforcing until the clock runs out. Start at a low max-age, confirm nothing broke, then raise it.

includeSubdomains, as woocommerce.com already sends, is stronger and correspondingly less forgiving. Every single subdomain must serve HTTPS correctly, including ones you may have long forgotten about.

Re-checking our own headers found a duplicate worth explaining

I re-ran this post’s own header check against sitea.example before expanding it, and found something the original table did not report: three of our own headers are sent twice, in the same response, with different values on one of them.

curl -sD - -o /dev/null https://sitea.example/ 
  | grep -iE 'strict-transport|x-content-type|x-frame'
x-content-type-options: nosniff
x-content-type-options: nosniff
x-frame-options: SAMEORIGIN
x-frame-options: SAMEORIGIN
strict-transport-security: max-age=63072000; includeSubDomains; preload
strict-transport-security: max-age=31536000

x-content-type-options and x-frame-options repeat with identical values, which is harmless, redundant rather than contradictory. The strict-transport-security pair does not match: one copy declares a two-year lifetime with includeSubDomains and preload, the other a one-year lifetime with neither. Two layers of this site’s own infrastructure, almost certainly Cloudflare adding its own edge-level header on top of whatever the origin application already sends, are each independently deciding this site’s HSTS policy without apparent awareness of the other.

This matters beyond tidiness because of exactly the irreversibility this post already warns about. max-age is a promise a browser remembers, and which of two conflicting values a given browser actually honours is not something this site’s operators currently control or have verified, since RFC 6797 specifies that a user agent should process only the first Strict-Transport-Security header it receives and ignore the rest, but header ordering across CDN and origin layers is not something most teams treat as a stable, deliberately-controlled property. Relying on which header happens to arrive first, decided by infrastructure layering rather than a deliberate choice, is the wrong way to control a setting this post already treats as hard to walk back once a browser has cached it.

The practical fix is not raising or lowering either number. It is finding the second source and removing it, so exactly one layer controls HSTS for this domain and the response carries one clear answer rather than two. Checking for this specific pattern, the same header appearing more than once with different values, is worth adding to the verification step this post already recommends: grep -c the header name in the response and treat anything above 1 as its own finding, independent of whether either individual value looks reasonable in isolation.

Setting the two safe ones

Configuration table showing the nosniff and referrer-policy headers set in Apache with Header always set, in nginx with add_header always, and in WordPress with a send_headers action, noted as a last resort
Prefer the server over WordPress. A header set in PHP is only sent on requests that

reach PHP, so a cached page or a static file served directly by the web server will not carry it. That gap is exactly where an uploaded file lives.*

That caption is the part people get wrong, and it matters more than the header choice. If your site is behind a page cache, headers added by a WordPress plugin may be absent on precisely the requests that matter. Set them at the web server or the CDN, then verify on a cached response.

The homepage-versus-upload gap, demonstrated on a real site

This post’s own caption warns that a header applied only in PHP will be missing on requests that never reach PHP, an uploaded file being the clearest example. I checked whether that gap actually shows up on a real site rather than only naming it as a theoretical risk.

curl -sI https://woocommerce.com/ | grep -iE 'x-content-type|x-frame|content-security'
curl -sI https://woocommerce.com/wp-content/uploads/2025/01/[email protected] 
  | grep -iE 'x-content-type|x-frame|content-security'

The homepage request returned x-frame-options, x-content-type-options, a referrer-policy and a minimal CSP. The uploaded JPEG returned nothing. Not a weaker version of the same protection, none of it: no nosniff, no frame protection, no CSP, on a static file served directly rather than through whatever application layer adds those headers to HTML responses.

That is the exact gap this post warns about, found on one of the four reference sites this post already treats as well-run. It is not necessarily a serious problem for this specific file, a JPEG has limited ways to be weaponised even without nosniff, but it demonstrates precisely why checking only the homepage, the request every quick audit defaults to, would have reported this site’s header posture as stronger than it actually is for the class of request nosniff exists to protect: a file matching an unexpected type. siteb.example, checked the same way, carried both nosniff and x-frame-options through to its own uploaded image correctly, proof the gap is not universal and specifically worth checking rather than assuming either way.

Verify, always

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

Run it against the homepage, then against an uploaded file such as /wp-content/uploads/2026/01/something.jpg. Those two responses often differ in practice, not only in theory, and the second one is where nosniff actually earns its place.

A header you added once and never checked again is not a header, not really. It is an intention, which is the same category error as a caching plugin that is active and caching nothing or a backup that has never been restored.

Protuno’s free audit reads all six of these from the domain alone, which is how the table above was built, so you can see the whole portfolio’s posture in one place. Rook is live now.

While you are reading headers, the same request tells you whether the server is announcing its PHP version to every visitor, which is a one-line fix in the same config file.

What changed on the other three sites since the original check

Worth reporting the full re-check rather than only the two findings above, since two of the three comparison sites had moved since this post first published. woocommerce.com now sends referrer-policy: strict-origin-when-cross-origin on its homepage, a header the original table recorded as present already, holding steady. siteb.example now sends x-frame-options and x-content-type-options on its homepage, both absent from the original table, which recorded only three headers there against four now. wordpress.org is the one holdout: still exactly x-frame-options and a one-hour strict-transport-security, unchanged in every respect from the original reading, including the one-hour HSTS lifetime this post already flags as too short to protect a returning visitor’s next first request.

That mix, one site improved, one held steady, one unchanged, is worth reading as evidence against treating any of this as settled once checked. A header posture is not a fact about a site, it is a fact about a specific request on a specific day, and three real, actively maintained sites produced three different trajectories over the same interval with no obvious pattern predicting which would move and which would not. The only way to know which category a given client site falls into is running the check again, not assuming the last result still holds because the site looks the same from the outside.

Set nosniff and a referrer policy on every site you look after this week. It is two lines, it cannot break anything, and on the four sites checked for this post, two were missing nosniff entirely. Whether that ratio holds for your own portfolio, and whether it has moved at all since the last time anyone looked, is exactly what re-running the loop above, on a schedule rather than once, will actually tell you.

Comments