Skip to content

WordPress security headers: HTTPS, HSTS and CSP step by step

Published 7 October 20269 min readBy the SmoothSeen editorial team

WordPress security headers are added at the web server, at the CDN or with a small plugin hooked to send_headers, because WordPress does not send them on public pages. Start by forcing HTTPS, then add HSTS with a short max-age, nosniff, Referrer-Policy, framing protection, Permissions-Policy and a Content Security Policy in report-only mode.

Key points

  • WordPress sends no security headers on public pages; it only protects the login screen and the dashboard with X-Frame-Options and, since 6.9, with CSP frame-ancestors as well.
  • Order matters: first HTTPS in both URLs under Settings > General and a 301 redirect on the server, then HSTS with a short max-age.
  • HSTS with preload forces HTTPS on every subdomain, requires a max-age of at least one year and is very hard to undo.
  • The inline scripts printed by WordPress and its plugins make a strict CSP hard work: start with Content-Security-Policy-Report-Only.
  • If you cannot touch the server, a mu-plugin on the send_headers hook adds headers from PHP, but not to static files.

To check it on your own site: SEO audit

On this page

A security header is a line in the HTTP response that tells the browser what it may and may not do with your page: whether it must always use HTTPS, whether another site may load it in an iframe, or where scripts may come from. WordPress controls the HTML it generates and a handful of headers in its dashboard. Everything else depends on where the site is served from: the web server, the CDN or the host. This guide explains what WordPress ships, what to add, with which value and where.

Which security headers does WordPress send on its own?

Very few, and almost none on the pages your visitors see. This is what the core code does on the 7.1 branch, the current one1:

Where
Login screen and dashboard
Header
X-Frame-Options: SAMEORIGIN and, since 6.9, Content-Security-Policy: frame-ancestors 'self'
WordPress function
send_frame_options_header()2
Where
Dashboard
Header
Referrer-Policy: strict-origin-when-cross-origin
WordPress function
wp_admin_headers()1
Where
REST API and admin-ajax.php
Header
X-Content-Type-Options: nosniff
WordPress function
send_nosniff_header() and the REST server1
Where
Public pages
Header
None of the above
WordPress function
—

In practice, your posts, pages and product pages go out without HSTS, without framing protection and without a CSP unless something adds them. If a header scan flags them as missing, it is not a theme bug: it is normal for WordPress.

Step 1: force HTTPS in WordPress and on the server

Without HTTPS the other headers achieve little: browsers ignore HSTS when it arrives over HTTP3. The switch has three parts:

  1. The certificate. Your host or server installs it. Check that https:// loads without warnings before going any further.
  2. Both WordPress URLs. Under Settings > General, change the WordPress Address (URL) and the Site Address (URL) to https://. If they are defined in wp-config.php with WP_SITEURL and WP_HOME, the fields are locked and you change them there4. Since 5.7, Site Health also offers a button to switch the whole site to HTTPS when it detects the server supports it, and rewrites http:// URLs in your content on the fly5.
  3. A 301 redirect on the server. WordPress redirects some URLs, but images, CSS and any file the server delivers directly never reach PHP. The redirect from http to https belongs on the server: step-by-step instructions are in the guides to .htaccess HTTPS and HSTS on Apache and HTTPS and HSTS on Nginx.

For logins and the dashboard, the WordPress documentation suggests the FORCE_SSL_ADMIN constant in wp-config.php, which forces logins and admin sessions over HTTPS6. If a reverse proxy or CDN terminates TLS and forwards requests to you over HTTP, the same page documents how to mark the request as secure to avoid a redirect loop6. This is that snippet with an extra check so it does not raise a notice when the header is missing:

define( 'FORCE_SSL_ADMIN', true );

// Only if a trusted proxy terminates TLS and forwards requests over http.
if ( isset( $_SERVER['HTTP_X_FORWARDED_PROTO'] )
	&& false !== strpos( $_SERVER['HTTP_X_FORWARDED_PROTO'], 'https' ) ) {
	$_SERVER['HTTPS'] = 'on';
}

Only use it if there really is a proxy in front: anyone can send X-Forwarded-Proto if the server can be reached directly.

Step 2: which headers to add, and with which value

This table gives a sensible starting point for WordPress, with the values documented on MDN:

Header
Strict-Transport-Security
Starting value
max-age=300, then max-age=31536000 once all is well
What it does
Makes the browser use HTTPS on later visits
Watch out for
Only counts when delivered over HTTPS3
Header
X-Content-Type-Options
Starting value
nosniff
What it does
Stops the browser guessing a file's type
Watch out for
Files must be served with the correct type
Header
Referrer-Policy
Starting value
strict-origin-when-cross-origin
What it does
Limits what is sent to other sites as the referring page
Watch out for
It is already the default in current browsers7; declaring it pins it
Header
X-Frame-Options
Starting value
SAMEORIGIN
What it does
Stops other sites loading yours in an iframe
Watch out for
ALLOW-FROM is obsolete; for lists of origins use frame-ancestors8
Header
Permissions-Policy
Starting value
camera=(), microphone=(), geolocation=()
What it does
Switches off browser features your site does not use
Watch out for
If a plugin shows a map with the visitor's location, do not block geolocation
Header
Content-Security-Policy-Report-Only
Starting value
A trial policy (see below)
What it does
Reports what a CSP would block, without blocking anything9
Watch out for
It ignores sandbox and cannot be set in a meta tag9

Two headers you should not add:

  • X-XSS-Protection: 1. MDN recommends CSP instead and warns that this header can create vulnerabilities on otherwise safe sites10. If your host sets it, the least risky value is 0.
  • HSTS with preload on day one. Getting onto the browsers' preload list requires includeSubDomains and a max-age of at least one year, and coming off the list is very hard3. It applies to every subdomain, including one that does not have a certificate yet. Only go there after months of HSTS without problems.

X-Frame-Options versus frame-ancestors

Both stop another site loading yours in an iframe. X-Frame-Options is the older one and only accepts DENY or SAMEORIGIN; the CSP frame-ancestors directive is the modern one and accepts a list of origins8. WordPress already sends both on the login screen and in the dashboard2. On public pages, while your CSP is still in report-only mode, keep X-Frame-Options: SAMEORIGIN, because frame-ancestors inside a report-only policy reports but does not protect.

Why is a strict CSP hard on WordPress?

A CSP (Content Security Policy) is the list of origins a page may load scripts, styles, images or frames from. By default, if the policy includes script-src, the browser will not run inline JavaScript, and MDN warns that 'unsafe-inline' defeats much of the purpose of having a CSP11.

WordPress pages carry a lot of inline JavaScript: from core, from themes and from almost every forms, analytics or cookie plugin. Since 5.7, WordPress has had functions and filters for adding attributes to the script tags it prints, and the announcement presented them as a path towards CSP in core, plugins and themes12. But plugins that write their own <script> tags do not go through them.

So the sensible order is:

  1. Publish the policy as Content-Security-Policy-Report-Only. It blocks nothing.
  2. Browse the site with developer tools open: every violation shows up in the console. If you want to receive reports, declare an endpoint with Reporting-Endpoints and the report-to directive9.
  3. Adjust the policy until your key pages are free of warnings.
  4. Only then switch it to Content-Security-Policy. Bear in mind that if two policies are present, the browser applies both and the stricter one wins11.

Where to set the headers: server, CDN or PHP

Where
Web server (.htaccess on Apache, add_header on Nginx)
Pros
Covers everything, including images, CSS and JS; does not depend on PHP
Cons
You need access to the configuration
Where
CDN or proxy
Pros
Managed from a dashboard, no server changes
Cons
Only covers what goes through the CDN; see the guide to security headers on Cloudflare
Where
PHP, on the send_headers hook
Pros
Works on any host
Cons
Only reaches the public pages WordPress generates

If you cannot touch the server or the CDN, a mu-plugin is the cleanest option. Must-use plugins are PHP files in wp-content/mu-plugins/ that WordPress always loads and that cannot be deactivated from the dashboard13. The send_headers action is for adding headers to the response and, since 6.1, it fires after the main query, so conditional tags work inside it14. Save this as wp-content/mu-plugins/security-headers.php:

<?php
/**
 * Plugin Name: Security headers
 * Description: Adds security headers to the pages WordPress generates.
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

add_action( 'send_headers', function () {
	if ( headers_sent() ) {
		return;
	}

	// HSTS over HTTPS only (browsers ignore it over HTTP).
	// Start with five minutes; raise it to 31536000 once everything runs on https.
	if ( is_ssl() ) {
		header( 'Strict-Transport-Security: max-age=300' );
	}

	header( 'X-Content-Type-Options: nosniff' );
	header( 'Referrer-Policy: strict-origin-when-cross-origin' );
	header( 'X-Frame-Options: SAMEORIGIN' );
	header( 'Permissions-Policy: camera=(), microphone=(), geolocation=()' );

	// CSP in report-only mode: blocks nothing, only reports in the console.
	header(
		"Content-Security-Policy-Report-Only: default-src 'self'; "
		. "script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; "
		. "img-src 'self' data: https:; font-src 'self' data:; "
		. "frame-ancestors 'self'; object-src 'none'; base-uri 'self'"
	);
} );

The sample CSP is a starting point, not a secure policy: it includes 'unsafe-inline' so you can first see what else your site loads. If you use fonts, video or analytics from other domains, the console will tell you which ones to add.

Three limits of this approach:

  • It does not cover static files (images, CSS, JS) that the server delivers without going through WordPress, nor the dashboard.
  • A page cache can bypass it. If your cache serves stored HTML without running PHP, the headers may be missing from cached pages. Check with curl on a page that is already cached.
  • One header, one place. If the server already sets X-Frame-Options and the mu-plugin does too, you end up with duplicate values. Decide where each header lives.

How to check it

Start by requesting the headers of a public page and of an image. If the page has them and the image does not, you are setting them from PHP:

curl -sI https://www.example.com/
curl -sI https://www.example.com/wp-content/uploads/2026/10/photo.jpg

Then:

  1. Run the home page through Mozilla's HTTP Observatory, which reviews security headers and the redirect to HTTPS15.
  2. Open the browser console on your key pages and look for report-only CSP warnings.
  3. Check Tools > Site Health for HTTPS notices.

What SmoothSeen does with this

Every SmoothSeen analysis checks whether the page is served over HTTPS, whether http redirects to https and which security headers it sends, using Mozilla's HTTP Observatory. The result feeds into the search visibility score and, if something is missing, it shows up as a task in the action plan with the reason.

What to do this week

Run curl -I against your home page and note which headers are missing. If you have server access, add them there; if not, install the mu-plugin with HSTS at five minutes and the CSP in report-only mode, and watch the console for a few days. To see the result alongside the other technical checks, analyse your site with SmoothSeen.

Frequently asked questions

Do I need a security plugin to add headers in WordPress?

No. A security plugin can do it, but so can the server, the CDN or a twenty-line mu-plugin you control. What matters is that each header is defined in one place only, because several plugins or layers adding the same header produce duplicate values that are hard to debug. If a plugin already manages them, check its values with curl -I.

When can I raise HSTS to a year?

Once you have run a short max-age for at least a few days without problems: every page loads over HTTPS, there is no mixed content and the subdomains you use have certificates. Then raise it to 31536000 seconds. Adding includeSubDomains or preload is a separate decision, because it affects every subdomain and preloading is very hard to reverse.

Why does my CSP break the WordPress dashboard?

Most likely because you set it on the server for the whole domain, including /wp-admin/, and the dashboard and block editor rely on inline JavaScript and styles. The mu-plugin in this guide avoids that because send_headers does not run in the dashboard. If you configure it on the server, limit it to the public site or try it in report-only mode first.

Is FORCE_SSL_ADMIN still needed if the whole site already uses HTTPS?

With both URLs on https:// and the redirect on the server, the dashboard already runs over HTTPS in practice. The constant adds a guarantee: WordPress forces HTTPS for logins and admin sessions even if another layer fails. It is one line in wp-config.php and the official documentation still recommends it, so it is worth keeping.

Sources

  1. 1WordPress 7.1 source: default-filters.php, admin-filters.php and functions.php, WordPress (official wordpress-develop repository), accessed 7 October 2026.
  2. 2send_frame_options_header(), WordPress Developer Resources, accessed 7 October 2026.
  3. 3Strict-Transport-Security, MDN Web Docs, accessed 7 October 2026.
  4. 4Settings General screen, WordPress.org Documentation, updated 3 July 2026.
  5. 5Improved HTTPS detection and migration in WordPress 5.7, Make WordPress Core, 22 February 2021.
  6. 6HTTPS, WordPress Developer Resources, updated 29 September 2025.
  7. 7Referrer-Policy, MDN Web Docs, accessed 7 October 2026.
  8. 8X-Frame-Options, MDN Web Docs, accessed 7 October 2026.
  9. 9Content-Security-Policy-Report-Only, MDN Web Docs, accessed 7 October 2026.
  10. 10X-XSS-Protection, MDN Web Docs, accessed 7 October 2026.
  11. 11Content-Security-Policy, MDN Web Docs, accessed 7 October 2026.
  12. 12Introducing script attributes related functions in WordPress 5.7, Make WordPress Core, 23 February 2021.
  13. 13Must Use Plugins, WordPress Developer Resources, updated 23 September 2026.
  14. 14send_headers (hook), WordPress Developer Resources, accessed 7 October 2026.
  15. 15HTTP Observatory, MDN (Mozilla), accessed 7 October 2026.

How to cite this article

SmoothSeen. (2026, October 7). WordPress security headers: HTTPS, HSTS and CSP step by step. https://smoothseen.com/en/blog/wordpress-security-headers/

Who writes this

SmoothSeen is a website audit tool that measures visibility in search engines and AI assistants and delivers reports under the agency's own brand.

This blog belongs to SmoothSeen: when an article discusses the product, it does so knowing the product is ours. Third-party figures link to their original source.

Change history

  • First version.

Keep reading

WordPress security headers: HTTPS, HSTS and CSP