Security headers every WordPress site should send
Six HTTP response headers that close common gaps on a WordPress site, with a copy-paste starting point and the order I roll them out in.
Why headers matter
A WordPress site is a stack of themes and plugins, each one loading scripts, styles and fonts from somewhere. Security headers are short instructions your server sends with every page that tell the browser what it is allowed to do with that content. They do not fix a vulnerable plugin, but they limit what a mistake or an injected script can do.
I set these up on the SunFish DataOn Philippines and GreatDay HR websites, including the Content Security Policy, X-Frame-Options and HSTS. Here is what each one does and the order I use so nothing breaks.
The six headers
- Strict-Transport-Security (HSTS). Tells browsers to use HTTPS only for your domain, even if someone types
http://. - Content-Security-Policy (CSP). Lists the sources allowed to load scripts, styles, images and frames. It is the strongest header here and the one most likely to break a page.
- X-Frame-Options. Stops other sites from putting yours in a frame, which blocks clickjacking. The CSP
frame-ancestorsrule does the same job in modern browsers, so I send both. - X-Content-Type-Options. The value
nosniffstops browsers from guessing a file's type, so an upload cannot be run as a script. - Referrer-Policy. Limits how much of your URL is passed to other sites when a visitor clicks away.
- Permissions-Policy. Switches off browser features you do not use, like the camera, microphone and location.
A starting point
On Apache or LiteSpeed hosting, which covers most shared hosts, add this to the .htaccess file in your site root. Keep a copy of the original first.
<IfModule mod_headers.c>
# Safe to add first
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=()"
# HSTS: start short, raise it once everything works over HTTPS
Header always set Strict-Transport-Security "max-age=300"
# CSP: report only at first, so nothing breaks while you learn what your site loads
Header always set Content-Security-Policy-Report-Only "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https:; frame-ancestors 'self'"
</IfModule>
Roll them out in this order
- Add the four simple headers:
nosniff, X-Frame-Options, Referrer-Policy and Permissions-Policy. These almost never cause problems. - Add HSTS with a short
max-age, like the 300 seconds above. Once every page and subdomain works over HTTPS, raise it to31536000(one year). - Add the CSP as
Content-Security-Policy-Report-Onlyand browse the whole site with the browser console open. - When the console stops showing blocked resources, switch the header name to
Content-Security-Policy.
Browsers remember it for the whole max-age. Add includeSubDomains only if every subdomain supports HTTPS, and skip preload until you are sure.
The catch with CSP on WordPress
Plugins inject inline scripts and pull resources from other domains, like Google Fonts, analytics and reCAPTCHA. A strict CSP will block them, and forms, sliders or checkout can quietly stop working. That is why the first version above only reports. Read what the console says is blocked, then allow only the sources you actually use.
The 'unsafe-inline' allowance in the example keeps most sites working, but it weakens the protection. The stricter step later on is to use nonces, which many security plugins and custom themes can generate.
How to check your work
- Scan the site with securityheaders.com or the Mozilla Observatory to see which headers are missing.
- Open the browser DevTools, go to the Network tab, click the page request and read the response headers.
- Test the contact form, search, login and any checkout after every change, in a private window.
Headers are cheap hardening, not a replacement for updates. Keep WordPress, themes and plugins current, remove what you do not use, and let the headers catch what slips through.