You have an SSL certificate. You run regular updates. You use a security plugin. All good, right?
Not quite. There’s a category of security measures that WordPress doesn’t set up on its own - and that most security plugins either don’t handle at all or handle incorrectly: HTTP security headers.
The problem isn’t that they’re hard to set up. The problem is that most WordPress site owners never find out they’re missing. The site runs, Google indexes it, nobody complains - and yet an important protection layer is missing, one that would prevent clickjacking, MIME sniffing, and certain XSS attacks.
In this post, I’ll explain what security headers are, which 8 of them matter for WordPress websites, and how to find out in about five minutes how your website scores.
What are HTTP security headers?
When a browser requests your website, the server doesn’t just send back HTML - it also sends a set of response headers. Most of these are technical in nature (content type, caching, redirects). Security headers are a subset of these that tell the browser how to treat the website.
Concretely, that means: you can tell the browser whether your site is allowed to be embedded in an iframe (relevant for clickjacking protection), whether it should “guess” the file type of resources itself or whether you forbid that, and which external sources are legitimately allowed to run scripts on your site.
The browser enforces these rules - automatically, without your WordPress code being involved at all.
That’s the advantage: if an attacker manages to inject malicious code into a plugin, a well-configured Content-Security-Policy header can still prevent that code from doing anything, because the browser blocks its execution.
The plugin problem
Now for the reason this topic so often falls through the cracks in the WordPress world.
There are plugins that set security headers. That sounds like a simple solution. But the reality is more complicated:
Plugins set headers via PHP. That happens late in the request - after WordPress core, after themes, after other plugins. If anyone in that chain overwrites the headers, your security headers are gone. You won’t see that in the admin dashboard.
Caching plugins can make the problem worse. If pages are cached before your security header plugin runs, pages may get served without the headers.
CDN and proxy layers. Many WordPress websites run behind Cloudflare or an nginx reverse proxy. Both can modify or remove headers. The plugin has no idea.
The result: your plugin shows you green checkmarks. But what the browser actually receives can look completely different. The only way to check that is from the outside - by fetching the real response headers that reach the visitor.
The 8 security headers that matter for WordPress
1. Content-Security-Policy (CSP)
The most powerful and complex header. It defines which resources the browser is allowed to load - and from where. A strict CSP prevents an attacker from running JS even if they managed to inject code into your site.
The catch: CSP is challenging to configure, because many WordPress themes and plugins load external resources. A CSP that’s too strict breaks the website; one that’s too loose doesn’t help much. Use our CSP Generator to build a clean policy.
Example value: Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-xyz'; img-src 'self' data:
2. Strict-Transport-Security (HSTS)
This header tells the browser: never load this website over HTTP - always HTTPS. That sounds trivial if you already have an SSL redirect. But without HSTS, the first request is still vulnerable (man-in-the-middle). With HSTS, the browser remembers after the first visit that it should always use HTTPS.
Important: max-age should be at least 31536000 seconds (1 year), and includeSubDomains should be set if you use subdomains.
Example value: Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
3. X-Frame-Options
This header prevents your website from being embedded in an <iframe> on someone else’s website. The classic attack scenario: clickjacking. The attacker invisibly overlays your website on top of another page, and the user clicks buttons on your website without knowing it.
For most WordPress websites, SAMEORIGIN is the right value - meaning only your own pages are allowed to embed it.
Example value: X-Frame-Options: SAMEORIGIN
4. X-Content-Type-Options
Browsers sometimes try to guess a resource’s file type themselves if the server doesn’t provide a clear Content-Type. This is called MIME sniffing. Attackers can exploit this by uploading a file so the browser interprets it as JavaScript, even though it’s officially declared as an image.
nosniff disables this behavior.
Example value: X-Content-Type-Options: nosniff
5. Referrer-Policy
When someone on your website clicks an external link, the browser by default sends the full URL of your page as the “referrer.” This can leak sensitive information (e.g. internal page names, parameters, login paths).
strict-origin-when-cross-origin is a good compromise: the referrer gets reduced to just the hostname when navigating to a different domain.
Example value: Referrer-Policy: strict-origin-when-cross-origin
6. Permissions-Policy
Formerly called “Feature-Policy.” This lets you define which browser features your website is allowed to use - and which aren’t. Camera, microphone, location, accelerometer. If your website doesn’t need any of these, you should explicitly disable them.
Example value: Permissions-Policy: camera=(), microphone=(), geolocation=()
7. X-XSS-Protection (deprecated)
This header still exists but is no longer recommended. It was meant for older browsers without their own XSS filter. Modern browsers ignore it or have removed it entirely. If you still have it set somewhere: replace it with a clean CSP.
8. Cross-Origin-Opener-Policy (COOP)
The newest of the eight. It isolates your browser tab from other tabs and prevents external pages you open from accessing your tab via JavaScript references. Relevant for websites with login or sensitive functionality.
Example value: Cross-Origin-Opener-Policy: same-origin
The mistake almost everyone makes
If you research this topic, you’ll quickly find .htaccess snippets like this:
<IfModule mod_headers.c>
Header set X-Frame-Options "SAMEORIGIN"
Header set X-Content-Type-Options "nosniff"
Header set Strict-Transport-Security "max-age=31536000"
</IfModule>
Paste it in, done, green checkmarks on securityheaders.com - that’s the common advice.
The problem: after adding it, nobody checks whether the headers actually reach the visitor. And they don’t always. A few typical scenarios where headers quietly disappear:
- Cloudflare overwrites or removes certain headers depending on your plan and configuration
- WP Rocket or W3 Total Cache serve cached pages before PHP-based headers can be set
- nginx as a reverse proxy in front of Apache has its own header configuration that takes priority
- Managed WordPress hosting (Kinsta, WP Engine, Flywheel) configures headers at the server level - sometimes correctly, sometimes not at all
You can’t resolve this by looking at your plugin dashboard or your .htaccess. You have to check from the outside.
How to check your security headers
That’s why I built the HTTP Security Headers Checker.
Enter a URL, run the check. The tool fetches your website’s real response headers server-side - meaning what an actual visitor’s browser really receives. It then analyzes all 8 headers, calculates a security score from 0-100, and shows you exactly what’s missing or poorly configured.
For every problematic header, you get ready-to-use code: as an .htaccess block, an nginx directive, or a PHP snippet for functions.php. Copy, add it in, check again.
The tool is free, requires no registration, and doesn’t store any content from your website - only the response headers are analyzed.
Setting up security headers in WordPress: three ways
If the check shows headers are missing, you essentially have three options:
Option 1: .htaccess (recommended for Apache hosting)
Add the header block to your .htaccess in the WordPress root directory, before the WordPress block. Works on most shared hosting plans and doesn’t depend on plugins.
<IfModule mod_headers.c>
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
Header always set X-Frame-Options "SAMEORIGIN"
Header always set X-Content-Type-Options "nosniff"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Permissions-Policy "camera=(), microphone=(), geolocation=()"
Header always set Cross-Origin-Opener-Policy "same-origin"
</IfModule>
Note always instead of set - that makes sure the headers get set on 3xx and 4xx responses too.
Option 2: functions.php (or a must-use plugin)
If you don’t have access to .htaccess or you’re using nginx, you can set headers via PHP:
add_action( 'send_headers', function() {
header( 'Strict-Transport-Security: max-age=31536000; includeSubDomains; preload' );
header( 'X-Frame-Options: SAMEORIGIN' );
header( 'X-Content-Type-Options: nosniff' );
header( 'Referrer-Policy: strict-origin-when-cross-origin' );
header( 'Permissions-Policy: camera=(), microphone=(), geolocation=()' );
header( 'Cross-Origin-Opener-Policy: same-origin' );
} );
Better to put this in a must-use plugin (/wp-content/mu-plugins/) instead of the theme’s functions.php - that way it survives theme switches and theme updates.
Option 3: nginx configuration
If you have access to the nginx server configuration (VPS, your own server), setting headers at the server level is the safest option - they get set independently of WordPress and can’t be overwritten by any plugin.
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
add_header X-Frame-Options "SAMEORIGIN" 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 Cross-Origin-Opener-Policy "same-origin" always;
After setup: check again
This sounds obvious but gets forgotten often: after every change, check again with the HTTP Security Headers Checker. Not because the code is wrong, but because caching, CDN layers, or hosting configurations can interfere.
It’s also worth checking again after major WordPress updates, plugin updates, or hosting migrations. Security headers can quietly disappear due to server configuration changes without anything visibly breaking.
Who should care most about security headers?
Technically, every public WordPress website should set security headers. In practice, there are cases where the effort is especially well justified:
WooCommerce shops process payment and customer data - here, HSTS and a strict CSP aren’t optional extras, they’re mandatory.
Membership websites and login areas benefit especially from X-Frame-Options and Cross-Origin-Opener-Policy, because clickjacking attacks often target login forms.
Websites with contact forms and user input should have a CSP that restricts inline scripts, to reduce XSS vectors.
Agencies and freelancers who run websites for clients should build security headers into their standard setup. It’s a small effort with a measurable result - and it’s easy to explain to clients when you can show a before/after score.
Set your security headers but want to make sure your CSP is configured cleanly? Use the CSP Generator from WP Helping Hand to build a WordPress-compatible Content Security Policy.


