WordPress

WordPress Security Headers: CSP, HSTS, and What to Actually Set

WordPress Security Headers: CSP, HSTS, and What to Actually Set

Security headers are one of those things that show up on every “WordPress hardening checklist” with almost no explanation of why some of them are trivial to set and others are genuinely difficult on a CMS built the way WordPress is. Here’s the honest version.

HSTS: the easy, well-defined one

HTTP Strict Transport Security tells browsers to only ever connect to your site over HTTPS, closing off a specific downgrade-attack window. The syntax, verified directly against MDN and hstspreload.org: Strict-Transport-Security: max-age=<seconds>; includeSubDomains; preload. If you want your domain on the browser-baked-in HSTS preload list, hstspreload.org’s own submission requirements are specific: a max-age of at least 31536000 seconds (one year), the includeSubDomains and preload directives both present, a valid certificate, and HTTPS working across every subdomain — not just the main one. MDN separately notes 63072000 seconds (two years) as a more common real-world value once you’re past initial preload submission. This one has essentially no downside once your whole domain is genuinely HTTPS-only, and most WordPress hosts or CDNs can set it at the server level without touching PHP at all.

CSP: the one that’s actually hard, and why

Content Security Policy restricts which scripts, styles, and resources a page is allowed to load, which is a powerful defense against injected malicious scripts — but it’s genuinely difficult to implement well on WordPress specifically. The core problem is inline JavaScript: page builders and many popular plugins generate inline <script> blocks dynamically, which defeats the static hash- or nonce-based allowlisting CSP is designed around. This isn’t a theoretical complaint — there’s a real, open GitHub issue on Elementor’s own repository requesting CSP header support, and a genuine, long-standing WordPress core Trac ticket (#39941) specifically about enabling CSP without relying on unsafe-inline, which defeats much of CSP’s actual protection.

WordPress core did take one real step here: a make.wordpress.org core post from February 23, 2021 introduced wp_get_script_tag, wp_print_inline_script_tag, a wp_script_attributes filter, and an wp_inline_script_attributes hook — explicitly framed as “a path forward” for CSP support, not full support itself. That’s the accurate way to describe where core stands: infrastructure for a future solution, not a finished one.

CSP isn’t hard on WordPress because nobody’s tried. It’s hard because a huge share of the plugin and page-builder ecosystem depends on inline scripts that CSP is specifically designed to restrict — and closing that gap requires ecosystem-wide changes, not just a header.

The other headers, and what they actually control

  • X-Content-Type-Options: nosniff — this has exactly one valid value, and it stops browsers from guessing a file’s content type in a way that can be exploited.
  • X-Frame-Options: DENY or SAMEORIGIN — prevents your site from being embedded in an iframe elsewhere, blocking clickjacking-style attacks. MDN’s own documentation notes this header is effectively superseded by CSP’s frame-ancestors directive where CSP is available, and that the older ALLOW-FROM value is obsolete and ignored by modern browsers.
  • Referrer-Policy — controls how much URL information gets sent to other sites when a visitor clicks a link away from you. strict-origin-when-cross-origin has been the browser default since around November 2020, and is a reasonable, privacy-conscious value to set explicitly if you want to be sure.
  • Permissions-Policy — restricts which browser features (camera, geolocation, fullscreen) a page and any embedded content are allowed to request, useful if you embed third-party content and want to limit what it can access.

How to actually set these on WordPress

The HTTP Headers plugin, with 50,000+ active installs, supports more than 30 header types including CSP, HSTS, and X-Frame-Options directly from the WordPress admin. For sites behind Cloudflare, WP Engine, Kinsta, or a similar host/CDN, setting these at the server or edge level rather than through a plugin is generally the more reliable path, since it applies before WordPress even loads — though I couldn’t independently verify each specific provider’s default header configuration, so check your own host’s actual behavior directly rather than assuming.

I want to be honest about a gap here: there’s no published, verifiable statistic for what percentage of WordPress sites actually have proper security headers configured. securityheaders.com doesn’t publish aggregate adoption data, and neither Sucuri nor Wordfence’s public reports surfaced this metric in what I could find. Don’t cite a specific adoption number if you see one floating around — it isn’t backed by anything I could trace to a real source.

A realistic starting order

  1. Set HSTS first — it’s low-risk, well-documented, and has no WordPress-specific complications once your site is genuinely all-HTTPS.
  2. Add X-Content-Type-Options, X-Frame-Options, and Referrer-Policy next — all three are straightforward, single-value settings with minimal risk of breaking anything.
  3. Treat CSP as a longer project, not a quick win. Start in report-only mode, expect friction from page builders and third-party embeds, and don’t expect a clean, complete policy without real testing time.

Most of the security-header checklist is genuinely easy to implement in an afternoon. CSP is the one exception, and it’s worth knowing that going in rather than getting stuck wondering why it’s breaking your page builder.

Rakibuzzaman Siam
Rakibuzzaman Siam Customer Experience Specialist at Rank Math, building AI automation projects on the side.