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

  1. Strict-Transport-Security (HSTS). Tells browsers to use HTTPS only for your domain, even if someone types http://.
  2. 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.
  3. X-Frame-Options. Stops other sites from putting yours in a frame, which blocks clickjacking. The CSP frame-ancestors rule does the same job in modern browsers, so I send both.
  4. X-Content-Type-Options. The value nosniff stops browsers from guessing a file's type, so an upload cannot be run as a script.
  5. Referrer-Policy. Limits how much of your URL is passed to other sites when a visitor clicks away.
  6. 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.

.htaccess
<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

  1. Add the four simple headers: nosniff, X-Frame-Options, Referrer-Policy and Permissions-Policy. These almost never cause problems.
  2. Add HSTS with a short max-age, like the 300 seconds above. Once every page and subdomain works over HTTPS, raise it to 31536000 (one year).
  3. Add the CSP as Content-Security-Policy-Report-Only and browse the whole site with the browser console open.
  4. When the console stops showing blocked resources, switch the header name to Content-Security-Policy.
HSTS is sticky.

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.

Share on LinkedIn
Written by Joshua Shallom Bugarin

Senior web developer and digital marketer in the Philippines. Currently Manager, Digital Marketing at NQX. About me or get in touch.