---
title: "WordPress Security Headers: CSP, HSTS and More Without Breaking Your Site"
description: "A tested starter set of WordPress security headers, plus a Content-Security-Policy that doesn't break the block editor, and the nginx trap to avoid."
url: "https://skybootstrap.com/wordpress-security-headers/"
canonical_url: "https://skybootstrap.com/wordpress-security-headers/"
date: "2026-10-27T13:40:00+06:00"
modified: "2026-10-27T11:43:31+06:00"
author: Skybootstrap Team
type: article
site: SkyBootstrap
language: en-US
---

# WordPress Security Headers: CSP, HSTS and More Without Breaking Your Site

Security headers tell the browser how to treat your pages, and the safe starter set is HSTS, X-Content-Type-Options, Referrer-Policy, and Permissions-Policy, added once at the server. The one to approach carefully is Content-Security-Policy: on our test site, a strict CSP broke the block editor completely, while a WordPress-aware policy left every admin screen working. This guide gives a tested starter set, shows how to add CSP without breaking the editor, and covers the nginx trap that silently drops your other headers.

## What WordPress already sends, and what’s missing

WordPress already sends a few security headers on its own, but only on admin and login pages, so your front end has none unless you add them. Knowing the gap tells you what to add.

**On our test site (test T1), the default WordPress 7.1.3 headers were:**

- `/wp-login.php` and `/wp-admin/`: `X-Frame-Options: SAMEORIGIN`, `Content-Security-Policy: frame-ancestors 'self'`, and `Referrer-Policy: strict-origin-when-cross-origin`.
- The REST API: `X-Content-Type-Options: nosniff`.
- The home page, posts, and uploaded files: nothing.

So WordPress protects its own login and admin from clickjacking (`X-Frame-Options`) but leaves your public pages bare. The `frame-ancestors` on wp-admin comes from core’s `send_frame_options_header()` (`wp-includes/functions.php`), added on `login_init` and admin screens only. Everything below is about covering the front end, where the gap is.

## The safe starter set

The safe starter set is four headers that improve security with almost no risk of breaking anything: HSTS, X-Content-Type-Options, Referrer-Policy, and Permissions-Policy. These don’t restrict what resources load, so they won’t break your theme or plugins; they only tell the browser to be stricter about transport, MIME types, referrers, and device features.

**For nginx, in your HTTPS server block:**

```
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
```

**What each does:**

- **Strict-Transport-Security (HSTS)** tells the browser to use HTTPS only, for `max-age` seconds (a year here). It stops downgrade attacks. Add `includeSubDomains` only if every subdomain is on HTTPS, and only turn on HSTS once HTTPS is solid, because it’s hard to undo. It requires HTTPS to be working first, covered in [moving WordPress to HTTPS](https://skybootstrap.com/wordpress-http-to-https/).
- **X-Content-Type-Options: nosniff** stops the browser guessing a file’s type, which closes some upload-based attacks.
- **Referrer-Policy** trims what your site sends in the `Referer` header to other sites. This matches what WordPress already uses internally.
- **Permissions-Policy** switches off browser features your site doesn’t use, like the camera and microphone, so a compromised script can’t reach them.

The `always` keyword matters: without it, nginx adds the header only to successful responses, not to error pages, which are exactly where you still want it.

One header you’ll see in old guides but should skip: `X-XSS-Protection`. MDN now warns it “can create XSS vulnerabilities in otherwise safe websites” and recommends a Content-Security-Policy instead ([MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/X-XSS-Protection)). Leave it off. Also skip `X-Frame-Options: ALLOW-FROM`, which MDN lists as obsolete and which modern browsers ignore; use CSP `frame-ancestors` for framing control.

## The nginx trap that drops your headers

In nginx, adding any `add_header` inside a `location` block cancels every `add_header` inherited from the server block, so a cache rule for images can silently strip your security headers from those files. This catches people constantly.

We reproduced it (test T2). With the four headers in the server block and a separate `Cache-Control` header added in the images `location`, the home page carried all four security headers, but image files carried only `Cache-Control`. The security headers were gone from exactly the files an attacker might target with a fake image.

The fix is to put the headers in a snippet and include it in every `location` that has its own `add_header`:

```
# /etc/nginx/snippets/security-headers.conf (the four headers from above)
# Then in the server block and in any location that adds its own header:
include snippets/security-headers.conf;
```

After moving the headers into a snippet and including it in the images location too, every response carried the full set, including the image files (test T3). The one duplicate to expect is `Referrer-Policy` on login and admin pages, where WordPress already sets it; a repeated identical value there is harmless. If you use nginx, assume this trap applies and check your image and static-file responses with `curl -I`.

## Content-Security-Policy: the one that breaks things

A Content-Security-Policy controls which scripts, styles, and images a page may load, and a strict one will break the WordPress block editor unless you write it to allow what WordPress needs. CSP is the most powerful header and the most dangerous to get wrong.

We proved both halves on the test site. A strict policy, `default-src 'self'`, in report-only mode flagged dozens of violations per page and, when enforced, broke the block editor outright: the editor screen showed “The block editor requires JavaScript” and never loaded (tests T4, T5b, with a screenshot in evidence). WordPress’s editor relies on inline scripts and styles that `default-src 'self'` forbids.

A WordPress-aware policy fixed it. This starter policy allows the inline scripts and styles WordPress needs while still blocking the dangerous defaults:

```
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https:; font-src 'self' data:; connect-src 'self'; frame-src 'self' blob:; worker-src 'self' blob:; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'self'; upgrade-insecure-requests" always;
```

With this enforced, the block editor and the site editor both loaded normally, and the only violation left was our deliberate third-party `http://` test script, which the policy correctly blocked (test T7b). The `upgrade-insecure-requests` directive at the end also doubles as a mixed-content backstop after an HTTPS move.

Two honest caveats. First, this policy includes `'unsafe-inline'` and `'unsafe-eval'` for scripts, which weakens the protection CSP is meant to give, because WordPress and most plugins depend on inline scripts. A truly strict, nonce-based CSP on WordPress is a large project and often breaks with each plugin update; this starter policy is a practical middle ground, not a maximal one. Second, your plugins may need more: a plugin that loads scripts from a CDN needs that host added to `script-src`. Which is why you roll it out in report-only mode first.

## How to roll out CSP without downtime

Deploy any CSP in report-only mode first, read the violations, widen the policy to cover what your real plugins need, and only then enforce it. Report-only tells the browser to report what it would block without actually blocking anything, so you find problems with no visitor ever seeing a broken page.

**The process:**

1. Add the policy with the header name `Content-Security-Policy-Report-Only` instead of `Content-Security-Policy`.
2. Browse your own site and admin, and watch the browser console (or a reporting endpoint) for violations. On our test, the starter policy in report-only mode showed zero violations on the post, login, dashboard, and both editors, and only a couple on the home page from the deliberate test resources (test T6).
3. For each real violation, add the needed source to the right directive (a CDN host to `script-src`, a font host to `font-src`).
4. When the console is clean across your real pages, rename the header to `Content-Security-Policy` to enforce it.

Test the finished headers with `curl -I https://yoursite.com` and a free header scanner. Headers are a cheap, high-value layer once they’re in place, but CSP is the one to treat with respect: report-only first, every time.

## Frequently Asked Questions

### Which security headers should a WordPress site use?

Start with four that carry almost no risk: Strict-Transport-Security (HSTS), X-Content-Type-Options, Referrer-Policy and Permissions-Policy. They don’t restrict what loads, so they won’t break your theme. Add a Content-Security-Policy last and carefully, since it can break the editor. Skip the obsolete X-XSS-Protection, which MDN warns can itself create vulnerabilities; use a Content-Security-Policy instead.

### Will a Content-Security-Policy break the WordPress block editor?

A strict one will. On our test site a default-src ‘self’ policy broke the block editor completely, because WordPress’s editor relies on inline scripts and styles that a strict policy forbids. A WordPress-aware policy that allows those inline scripts left every admin screen working. Always deploy any CSP in report-only mode first, widen it to fit your plugins, then enforce it.

### Why did my security headers disappear from some pages in nginx?

Because of how nginx inherits headers: adding any add_header inside a location block cancels every add_header from the server block. On our test site, a cache rule in the images location stripped the security headers from image files. The fix is to put the headers in a snippet and include it in every location that adds its own header, so they appear on every response.

### Do I still need X-Frame-Options with WordPress?

WordPress already sends X-Frame-Options: SAMEORIGIN on its login and admin pages, from core, so you don’t need to add it there. For your front end, the modern equivalent is the Content-Security-Policy frame-ancestors directive, which the starter policy in this guide includes. The obsolete ALLOW-FROM value of X-Frame-Options is ignored by modern browsers, so don’t rely on it.

## What headers to add, in order

Add headers working from safe to risky:

1. **The four starter headers** (HSTS, nosniff, Referrer-Policy, Permissions-Policy), at the server, with `always`. Low risk, do these first. Turn on HSTS only after HTTPS is solid.
2. **If you use nginx,** put them in a snippet and include it in every `location` with its own header, or they’ll vanish from those responses.
3. **CSP last,** in report-only mode, widened to fit your plugins, then enforced. Start from the WordPress-aware policy above, not from `default-src 'self'`.

Headers are one layer among several. They sit on top of HTTPS (our moving WordPress to HTTPS guide) and account-level protection ([auditing user accounts](https://skybootstrap.com/wordpress-user-security/)); the full order is in [the full WordPress security checklist](https://skybootstrap.com/wordpress-security-checklist/).