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
- Which security headers does WordPress send on its own?
- Step 1: force HTTPS in WordPress and on the server
- Step 2: which headers to add, and with which value
- Why is a strict CSP hard on WordPress?
- Where to set the headers: server, CDN or PHP
- How to check it
- What SmoothSeen does with this
- What to do this week
- Frequently asked questions
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: SAMEORIGINand, 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:
- The certificate. Your host or server installs it. Check that
https://loads without warnings before going any further. - Both WordPress URLs. Under Settings > General, change the WordPress Address (URL) and the Site Address (URL) to
https://. If they are defined inwp-config.phpwithWP_SITEURLandWP_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 rewriteshttp://URLs in your content on the fly5. - 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
httptohttpsbelongs 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, thenmax-age=31536000once 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-FROMis obsolete; for lists of origins useframe-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
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 is0.- HSTS with
preloadon day one. Getting onto the browsers' preload list requiresincludeSubDomainsand amax-ageof 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:
- Publish the policy as
Content-Security-Policy-Report-Only. It blocks nothing. - 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-Endpointsand thereport-todirective9. - Adjust the policy until your key pages are free of warnings.
- 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 (
.htaccesson Apache,add_headeron 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_headershook - 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
curlon a page that is already cached. - One header, one place. If the server already sets
X-Frame-Optionsand 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.jpgThen:
- Run the home page through Mozilla's HTTP Observatory, which reviews security headers and the redirect to HTTPS15.
- Open the browser console on your key pages and look for report-only CSP warnings.
- 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
- 1WordPress 7.1 source: default-filters.php, admin-filters.php and functions.php, WordPress (official wordpress-develop repository), accessed 7 October 2026.
- 2send_frame_options_header(), WordPress Developer Resources, accessed 7 October 2026.
- 3Strict-Transport-Security, MDN Web Docs, accessed 7 October 2026.
- 4Settings General screen, WordPress.org Documentation, updated 3 July 2026.
- 5Improved HTTPS detection and migration in WordPress 5.7, Make WordPress Core, 22 February 2021.
- 6HTTPS, WordPress Developer Resources, updated 29 September 2025.
- 7Referrer-Policy, MDN Web Docs, accessed 7 October 2026.
- 8X-Frame-Options, MDN Web Docs, accessed 7 October 2026.
- 9Content-Security-Policy-Report-Only, MDN Web Docs, accessed 7 October 2026.
- 10X-XSS-Protection, MDN Web Docs, accessed 7 October 2026.
- 11Content-Security-Policy, MDN Web Docs, accessed 7 October 2026.
- 12Introducing script attributes related functions in WordPress 5.7, Make WordPress Core, 23 February 2021.
- 13Must Use Plugins, WordPress Developer Resources, updated 23 September 2026.
- 14send_headers (hook), WordPress Developer Resources, accessed 7 October 2026.
- 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/
Keep reading
What is SEO? How search engine optimisation works and how to improve it in 2026
What SEO is, how Google decides which pages to show and a prioritised checklist to improve your rankings with free tools.
.htaccess force HTTPS: redirect to https, enable HSTS and add security headers in Apache
How to force HTTPS in .htaccess or an Apache VirtualHost, roll out HSTS safely and add security headers. Every snippet tested on Apache 2.4.69.
.htaccess gzip and Brotli: browser caching and blocking AI bots in Apache
How to enable gzip and Brotli, set browser caching and block AI training bots in Apache .htaccess without dropping out of ChatGPT search. Tested.