To add HTTP security headers to WordPress, set them once at the server level, on Apache, Nginx or Cloudflare. Start with five safe headers. Then add a Content Security Policy in Report-Only mode, fix what the browser console flags, and turn on enforcement last.
Most guides give you a block of code to paste into .htaccess and stop there. That breaks down at CSP, where one wrong rule can kill your checkout. So we ordered this guide by risk.
One limit first. Headers protect your visitor's browser. They do not clean malware or patch a weak plugin.
What Security Headers Do (and Which Ones to Skip)
HTTP security headers are instructions your server sends with every page. They tell the visitor's browser what to allow, what to block and how to connect. The browser follows them before it runs any code on the page.
Six headers matter for a WordPress site.
We use SAMEORIGIN over DENY because DENY also stops your own site from framing its pages.
Skip These in 2026
X-XSS-Protection. Major browsers removed the filter it controlled. OWASP says send 0 or leave it out, and MDN says use CSP instead. Many older guides still tell you to add it.
Expect-CT. Browsers now check certificate transparency on their own.
Public-Key-Pins. Browsers dropped it, and one wrong pin could lock visitors out.
Feature-Policy. Permissions-Policy replaced it.
Headers are one control among several. Our OWASP Top 10 applied to WordPress walks through where they sit next to the rest of your risk list.
You can send the same headers from five places. Each one has a catch.
If you already use Cloudflare to block countries or IPs, Transform Rules sit in the same dashboard.
With server access, use Apache or Nginx. Without it, use Cloudflare or your host. Then pick one layer and stay there. Two layers can send the same header twice, and the browser may not follow the one you meant.
Four of these are add-and-forget. HSTS needs a slower rollout, which we cover after the code.
For Apache, add this above # BEGIN WordPress in your .htaccess file. WordPress rewrites everything between its own markers, so a header placed there can disappear.
<IfModule mod_headers.c>
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 X-Frame-Options "SAMEORIGIN"
Header always set Strict-Transport-Security "max-age=86400"
</IfModule>
For Nginx, add this inside your HTTPS server block. Run nginx -t, then reload.
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 X-Frame-Options "SAMEORIGIN" always;
add_header Strict-Transport-Security "max-age=86400" always;
↗️
Need Help Cleaning Your Site?
Our security experts remove malware, fix vulnerabilities, and protect your site.
Get Free Security Check
The always flag sends each header on error pages too.
Roll Out HSTS in Stages
HSTS tells browsers to refuse plain HTTP for your domain. If any part of your site still runs on HTTP, visitors get locked out until the header expires. So start small.
Set max-age=86400 for one day.
After a week without problems, raise it to 31536000 for one year.
Add includeSubDomains only when every subdomain runs HTTPS.
Read the HSTS preload rules before you add preload.
Add a Content Security Policy Without Breaking Your Site
CSP is the header built to stop cross-site scripting, and it is also the header most likely to break your site. So we add it in two halves.
Step 1. Send the Safe Half Now
Three directives need no allowlist, so they rarely block anything you use. frame-ancestors controls which sites can frame your pages. object-src 'none' blocks old embedded objects. base-uri 'self' limits the base tag to your own site.
Header always set Content-Security-Policy "frame-ancestors 'self'; object-src 'none'; base-uri 'self'"
Step 2. Test the Resource Rules in Report-Only
Now add the rules for scripts, styles, images and fonts. Send them as Content-Security-Policy-Report-Only. The browser blocks nothing. It only logs what it would block. This header can run beside the enforced one.
Header always set Content-Security-Policy-Report-Only "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self'; frame-src 'self'"
The starter policy allows inline styles because themes and page builders rely on them. Visit a post, the contact form, checkout, the login page and the editor. Expect noise, because themes and plugins print inline scripts. Add an origin only if you recognise it.
Step 3. Enforce, Then Tighten
When the console goes quiet, rename the header to Content-Security-Policy. Clear every cache layer, then test again.
Inline scripts are the hard part. 'unsafe-inline' in script-src is the quick fix, but it lets injected inline code run too. Hashes suit inline scripts that never change. Nonces are stronger, but a nonce must change on every request and a full page cache serves one saved copy to everyone.
The Strict CSP plugin applies a nonce-based policy to the front end and login screen. It blocks any script a theme or plugin prints by hand. Check how it works with your page cache before you rely on it.
Keep wp-admin Out of It
Send the resource rules on public pages only. On /wp-admin/, send just the safe half from Step 1. The Strict CSP plugin's own documentation says its policy cannot yet be applied to wp-admin, and points to WordPress core ticket #59446.
Headers protect the browser. They never touch your server.
Two of the things we found during our Fake Cloudflare CAPTCHA case study were a self-hiding backdoor plugin and five rogue administrator accounts dating back to March 2025. Both live on the server. No header touches either one. A CSP can limit which domains a page's scripts can call. It cannot stop a script from sending a visitor to another page.
Report-Only mode has a side benefit here. Every unknown domain in your reports is worth a second look, because it is sometimes an old tracking tag and sometimes injected code. Our guide to the signs of a hacked site shows how to tell the difference.
Test the live response, not your settings screen. Run this from a terminal.
curl -I https://example.com/
Check five URLs. Use the homepage, a post, /wp-login.php, /wp-admin/ and one image or script file. Headers can differ by URL, and a header set by a plugin may not reach static files.
Then scan the site with securityheaders.com. Open your browser's Network tab too, and click the page request to see its response headers.
A grade proves a header exists. It does not prove your checkout still works. So test your checkout and contact form as well.
Add the five safe headers this week and send the safe half of CSP with them. Run the rest in Report-Only mode for a week or two, then enforce it once the console goes quiet.
Headers are one layer. We check that layer, along with the files, users and plugins behind it, whenever we run a WordPress malware removal and security cleanup for a client.