WordPress Security

HTTPS, HSTS and Security Headers for WordPress Explained

Finish HTTPS properly, clear mixed content, roll out HSTS without trapping yourself, add the three safe headers, and decide how far to take Content Security Policy on a WordPress site.

HTTPS, HSTS and Security Headers for WordPress Explained

Run a scanner over a default install and it will mark the site down for missing WordPress security headers. Some of those findings matter and take one line to fix. One of them, Content Security Policy, can break your checkout if you paste in a policy from somebody else's site. None of them help while the HTTPS underneath is half-configured.

The order that works on a real site is to finish HTTPS, clear mixed content, and then add headers one at a time, starting with the ones that cannot hurt.

Finish HTTPS before you touch headers

Four things have to be true, and the padlock on the home page proves none of them.

  • The certificate covers both the bare domain and the www version, and it renews automatically.
  • Under Settings > General, the WordPress Address and the Site Address both begin with https://. When WordPress detects that HTTPS is available, Tools > Site Health offers a button that switches both for you.
  • Every HTTP request is redirected to HTTPS with a single 301 at the server or CDN, not through a chain of two or three hops.
  • wp-config.php contains define( 'FORCE_SSL_ADMIN', true ); so logins and admin sessions never run unencrypted.

A site behind a load balancer, a CDN or a reverse proxy that handles the certificate has one extra problem. WordPress receives plain HTTP from the proxy and may answer with endless redirects. The official HTTPS documentation describes the fix, which goes in wp-config.php above the "stop editing" comment:

if ( isset( $_SERVER['HTTP_X_FORWARDED_PROTO'] ) && 'https' === $_SERVER['HTTP_X_FORWARDED_PROTO'] ) {
    $_SERVER['HTTPS'] = 'on';
}

Add it only when the site really is behind such a proxy. The symptoms and other causes are in our article on fixing the WordPress login redirect loop.

Track down mixed content

Mixed content is an HTTPS page that loads something over plain HTTP. Browsers refuse insecure scripts, stylesheets and frames, and they upgrade or drop insecure images. The result is a broken layout, a missing slider or a padlock with a warning.

The old addresses hide in post content, page builder data, theme options and widgets, and sometimes in a theme file where somebody typed a full URL. Open the browser console on a few different page types and it will list each blocked request. Then fix the database copies in one pass with WP-CLI, which handles serialized data correctly:

wp search-replace 'http://example.com' 'https://example.com' --skip-columns=guid --dry-run

Read the report, take a database backup, and run it again without --dry-run. Third-party resources that are still requested over HTTP need their address changed by hand, or the embed replaced. Clear every cache when you are done and check the console again.

HSTS: strong protection that is slow to undo

The Strict-Transport-Security header tells a browser to use HTTPS for your domain for a set number of seconds, whatever link the visitor follows. After the first visit, it closes the gap in which a request could start over HTTP and be intercepted before the redirect. It also removes the option to click past a certificate warning, so an expired certificate turns into a site nobody can open. The MDN reference for Strict-Transport-Security covers the details. Four cautions come first:

  1. Ramp up. Start with max-age=300, which is five minutes. When nothing breaks, move to a week, then a month, then a year (max-age=31536000), waiting out each stage before the next.
  2. Think before adding includeSubDomains. It applies the rule to every subdomain: the old intranet, a staging site, a device or tool that only speaks HTTP. List your subdomains first.
  3. Treat preload as permanent. It asks browser vendors to ship your domain in a built-in HTTPS-only list. Getting off that list takes months to reach users. Add it only when every subdomain will stay on HTTPS for good.
  4. Know the way back. Sending max-age=0 over HTTPS clears the policy in each browser on its next visit. Browsers ignore the header on HTTP responses.

Three WordPress security headers you can add today

HeaderStarting valueWhat it doesWhat could break
X-Content-Type-OptionsnosniffStops the browser guessing a file's type, so an upload labeled as an image is not run as a scriptAlmost nothing
X-Frame-OptionsSAMEORIGINStops other sites loading your pages inside a frame, the basis of clickjackingPartners who embed your pages on their domain
Referrer-Policystrict-origin-when-cross-originSends other sites your domain only, never the full address of the page the visitor came fromAlmost nothing

WordPress already sends frame protection on the login and admin screens, and a referrer policy across the admin. The front end is left to you.

Permissions-Policy can switch off browser features the site never uses, such as the camera, microphone and location. X-XSS-Protection is deprecated, so do not add it because a scanner asks. The OWASP Secure Headers Project keeps a current list of which headers are worth sending.

Content Security Policy on WordPress, realistically

A Content Security Policy tells the browser which sources of scripts, styles, frames and other resources a page may use. It is the strongest header against cross-site scripting, and the hardest to deploy on WordPress.

The difficulty is inline code. Core, themes and plugins print inline scripts and styles all over a page: settings passed to scripts, block styles, analytics snippets, page builder output. A strict policy has to approve each one with a nonce or a hash. WordPress has filters such as wp_inline_script_attributes for adding attributes to the tags it prints, but many plugins write their own tags, and a page cache that stores finished HTML conflicts with a nonce that should change on every request. A policy that allows all inline scripts deploys easily and gives up most of the protection.

What we would do on a typical business site:

  1. Enforce a few directives that rarely break anything: frame-ancestors 'self', base-uri 'self', object-src 'none' and upgrade-insecure-requests. The first is the modern form of X-Frame-Options, and the last asks the browser to fetch leftover HTTP resources over HTTPS.
  2. Send a fuller policy as Content-Security-Policy-Report-Only on the front end. Nothing is blocked. The browser console shows what would have been.
  3. Browse the site for a couple of weeks with that header on, including checkout, forms and embedded video, and add each legitimate source. Payment and embed providers publish the domains they need.
  4. Enforce it on the front end only. Leave the admin area alone, because the editor and most plugins need looser rules there.

Be honest about the upkeep. Every new plugin, marketing tag or embedded widget can violate the policy, so somebody has to own it. For many sites, step one plus disciplined updates is a reasonable place to stop.

Where to set headers, and what goes wrong

Set each header in one place. The same header sent twice with different values behaves unpredictably.

At the CDN or proxy. This covers every response, including cached pages and static files, and is the simplest choice when you have one.

In Apache, in the root .htaccess outside the WordPress block:

<IfModule mod_headers.c>
    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 Strict-Transport-Security "max-age=300" "expr=%{HTTPS} == 'on'"
</IfModule>

In nginx, in the server block for the HTTPS site:

add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Strict-Transport-Security "max-age=300" always;

Watch for one nginx behavior: a location block that contains any add_header line of its own stops inheriting these. Put them in a small file and include it wherever headers are set.

In PHP, through the send_headers action in a must-use plugin, or through a headers plugin. This needs no server access, but it only reaches responses that WordPress generates. Images, CSS files and pages served straight from a full-page cache will not carry the header.

Verify from outside with curl -sI https://example.com/ and read the response. Repeat it for an inner page, a static file and a page that does not exist, since error pages are where headers most often go missing.

How far to take it yourself

HTTPS, mixed content, the three safe headers and the first HSTS stages are within reach of anyone with access to the hosting panel and a free afternoon. Ask for help when HSTS has to cover a domain with many subdomains, when headers are being set in three layers that disagree, or when you want an enforced Content Security Policy on a store or a learning site, where a mistake blocks payments or video.

Setting these once is the easy part. Keeping them correct as plugins and embeds change is ongoing work, and it fits naturally into a WordPress maintenance plan. Headers are also only one group of checks. The rest are in our WordPress security checklist.

Frequently asked questions

Can I add security headers with a plugin?

Yes. A headers plugin sends them from PHP, which is fine for pages WordPress builds. It will not cover static files or pages served directly by a full-page cache or CDN, so check with curl after setting it up and move the headers to the server or CDN if any are missing.

Will HSTS break my site?

Only if some part of the domain still needs plain HTTP, or if the certificate is allowed to expire. That is the reason to start with a five-minute max-age and to leave out includeSubDomains until every subdomain has been checked.

Do I still need X-Frame-Options if I send frame-ancestors?

Modern browsers follow frame-ancestors and ignore the older header when both are present. Sending both does no harm and covers older browsers, which is what WordPress itself does on its login and admin screens.

Why does a scanner still give my site a low grade?

Usually because there is no enforced Content Security Policy. A grade measures which headers are present, not how likely your site is to be attacked. A site with current plugins and no CSP is in better shape than one with a perfect grade and a year of pending updates.

Add WorldWin Coder to your preferred sources

Add worldwincoder.com as a preferred source on Google, or open this article in your AI assistant to use it as a source.

Swati Priyadrshni
· Founder & CEO at WorldWin Coder

I'm Swati Priyadarshani, Founder & CEO of LMS Advisor and Co-Founder of WorldWin Coder Pvt. Ltd. (est. 2019). Over the last 10+ years, I've helped enterprises build learning platforms that actually get used - not just deployed.

Keep reading

Have something to build, fix or move?

Tell us where you are. A developer reads every inquiry, and you get next steps and a free quote within one business day.