WordPress Security

HTTPS and Security Headers for WordPress

Getting HTTPS right on WordPress, fixing mixed content, and which security headers are worth setting — including how to deploy a CSP without breaking the site.

By 5 min read
Shield with a tick mark over three stacked layers

HTTPS stopped being optional years ago, and most WordPress sites have it. What they often don't have is a clean HTTPS setup, and almost none have security headers beyond whatever their host sets by default.

These are cheap wins. Here's the order I'd do them in.

Getting HTTPS right

  • A valid certificate, renewed automatically. Free automated certificates are standard now and an expired certificate is an entirely avoidable outage.
  • WordPress Address and Site Address both set to the https:// form in Settings → General.
  • A server-level redirect from HTTP to HTTPS, so no request is answered in the clear.
  • One canonical hostname — pick www or bare and redirect the other, at the same layer.
  • Only one system enforcing each of those. WordPress forcing HTTPS while the server also forces it is the standard recipe for a redirect loop.

Mixed content

Mixed content is an HTTPS page loading a resource over HTTP. Browsers block scripts and stylesheets loaded this way outright and warn about images, which is why a migrated site sometimes looks broken in ways that make no sense.

The usual sources are hard-coded HTTP URLs in post content from before the migration, plugin settings storing an HTTP URL, theme code with a protocol written into it, and external embeds from services that don't support HTTPS.

Fix it at the source: a proper search-and-replace across the database, using a tool that handles serialised data correctly — a plain SQL find-and-replace corrupts serialised arrays. Then fix the theme and plugin settings.

The upgrade-insecure-requests directive can paper over the rest, but treat it as a safety net rather than the fix. A resource that genuinely isn't available over HTTPS will still fail.

The headers worth setting

Four of these are quick, uncontroversial, and unlikely to break anything.

  • Strict-Transport-Security tells browsers to use HTTPS for your domain for a given period, so a later HTTP request never happens at all. Start with a short max-age, confirm nothing breaks, then increase it. Be careful with includeSubDomains if any subdomain isn't on HTTPS.
  • X-Content-Type-Options: nosniff stops browsers guessing a file's type, which prevents a class of attack where an uploaded file is interpreted as something executable.
  • X-Frame-Options: DENY, or frame-ancestors in a CSP, stops your pages being embedded in someone else's frame — which is how clickjacking works.
  • Referrer-Policy: strict-origin-when-cross-origin stops full URLs leaking to external sites, which matters when URLs contain anything identifying.
  • Permissions-Policy lets you switch off browser features the site doesn't use — camera, microphone, geolocation — so a compromised script can't ask for them.

Content Security Policy

CSP is the powerful one and the one that takes real work. It tells the browser exactly which sources of script, style, images and connections are allowed, which turns a cross-site scripting hole from a compromise into a blocked request.

It's also the header most likely to break your site, because WordPress sites load scripts from more places than anyone remembers: analytics, tag managers, chat widgets, embedded video, payment gateways, and whatever the theme bundles.

Deploying a CSP without breaking things

  • Start with Content-Security-Policy-Report-Only. The browser reports violations without blocking anything, so you can see what your policy would break before it breaks it.
  • Collect reports for a week or two, across all your templates and including the checkout if there is one.
  • Build the policy from what you actually observe, not from what you think the site loads.
  • Avoid unsafe-inline for scripts, which defeats most of the point. Use hashes or nonces for the inline scripts you genuinely need.
  • Expect the admin to need a different, looser policy than the front end. The block editor does things a strict policy dislikes.
  • Switch to enforcing mode only when the report-only mode has been quiet for a while, and keep a reporting endpoint after you do.

Where to set headers

At the server or CDN level if you can — a hosting configuration file, an Nginx block, or your CDN's rules. The headers are then applied to every response including static files, and there's no PHP involved.

WordPress can send them from code, hooked into the response, which is portable and version controlled but only covers PHP-rendered responses.

Don't set them in two places. Duplicate headers with different values behave unpredictably across browsers, and it's a genuinely confusing thing to debug.

Cookies

  • Secure, so cookies are never sent over HTTP.
  • HttpOnly on session cookies, so JavaScript can't read them — this limits the damage from an XSS hole considerably.
  • SameSite=Lax as a sensible default, which blocks cookies on most cross-site requests and prevents a class of CSRF.
  • WordPress sets reasonable defaults once the site is properly on HTTPS, but plugins setting their own cookies often don't. Check what's actually being set.

Verifying

  • Run your site through an online header scanner, which grades what you're sending and explains the gaps.
  • Check the response headers in the browser's network panel on a real page, not just the homepage.
  • Confirm HTTP redirects to HTTPS in one hop, not two or three.
  • Check for mixed content warnings in the console across several templates.
  • Verify the certificate chain is complete — a chain missing an intermediate works in some browsers and fails in others, which is a maddening bug to diagnose.
  • Re-check after any host, CDN or plugin change. Headers get dropped silently.

What headers don't do

They're defence in depth, not a substitute for the basics. A site with a perfect header grade and an outdated plugin is still going to be compromised through the plugin.

Get the fundamentals right first: updates, two-factor authentication on administrators, tested backups, minimal plugins. Then add headers, which take an afternoon and make a whole class of attack harder.

Frequently asked questions

How do I fix mixed content warnings in WordPress?

Find the HTTP URLs and change them. Run a proper search-and-replace across the database with a tool that handles serialised data — a plain SQL find-and-replace corrupts serialised arrays — then check theme code and plugin settings. Treat upgrade-insecure-requests as a safety net, not the fix.

Which security headers should a WordPress site have?

Start with four that take minutes and rarely break anything: Strict-Transport-Security, X-Content-Type-Options, X-Frame-Options and Referrer-Policy. Add Permissions-Policy to switch off browser features you don't use. A Content Security Policy is the powerful one, and it needs real work.

Will a Content Security Policy break my WordPress site?

It will if you deploy it enforced without testing. Start with Content-Security-Policy-Report-Only, collect violations across every template for a week or two, and build the policy from what you actually observe. Expect the admin to need a looser policy than the front end.

Should I set security headers in WordPress or on the server?

Server or CDN level if you can — the headers then apply to every response including static files, with no PHP involved. Setting them in WordPress is portable and version controlled but only covers PHP-rendered responses. Never do both: duplicate headers with different values behave unpredictably.

Is HSTS safe to enable?

Yes, with care. Start with a short max-age, confirm nothing breaks, then increase it. Be cautious with includeSubDomains if any subdomain isn't on HTTPS, and think hard before preloading — removal from the preload list is slow, so a mistake is hard to undo.

Topics

  • WordPress HTTPS
  • security headers
  • Content Security Policy
  • mixed content WordPress