Skip to content
  • CS-12
  • Reference
  • Last checked November 7, 2026

Security Headers and WordPress Hardening Reference

Every common security header with a baseline value and WordPress caveats, from HSTS and CSP to cookies and cross-origin policies, plus a hardening checklist and the security.txt format.

Run the health check

Security headers tell the browser how to treat your pages: only connect over HTTPS, refuse to be framed by other sites, run only scripts you trust, and keep cookies away from scripts and other sites. They cost nothing to send, but a strict policy copied without testing can break the block editor, embeds, payment pop-ups or analytics. This reference gives a baseline value for each header, explains the options, and flags what tends to break on WordPress. The second half is a hardening checklist with the risk of each change.

Placeholders: EXAMPLE.COM is your domain, CDN.EXAMPLE.COM a third-party origin you load from, RANDOM-NONCE a value generated fresh for each response, and REPORT-ENDPOINT the URL that collects violation reports. CS-07 shows how to send headers from Apache and Nginx (including the Nginx add_header inheritance trap). Check what a page sends with curl -sI https://EXAMPLE.COM/.

Header quick reference

Header Baseline value Protects against WordPress risk
Strict-Transport-Security max-age=31536000; includeSubDomains Protocol downgrade, cookie theft over HTTP Breaks any subdomain still on HTTP when includeSubDomains is set
Content-Security-Policy Start with frame-ancestors 'self'; base-uri 'self'; object-src 'none'; upgrade-insecure-requests Cross-site scripting, data injection, clickjacking High for script and style rules; low for this starter
X-Content-Type-Options nosniff MIME sniffing of uploads into scripts or styles Low: only breaks files served with the wrong Content-Type
X-Frame-Options SAMEORIGIN Clickjacking Low: blocks legitimate framing by other sites
Referrer-Policy strict-origin-when-cross-origin Leaking full URLs (with query strings) to other sites Low
Permissions-Policy camera=(), microphone=(), geolocation=() Misuse of powerful browser features by embedded or injected code Medium if you also restrict features embeds need (fullscreen, autoplay)
Cross-Origin-Opener-Policy same-origin-allow-popups (or leave unset) Cross-window attacks, some side channels Medium: same-origin breaks payment and sign-in pop-ups
Cross-Origin-Embedder-Policy Leave unset unless you need cross-origin isolation Enables isolation features High: require-corp blocks third-party embeds and images
Cross-Origin-Resource-Policy same-site on private resources Other sites loading your resources (side channels) Medium if set site-wide: breaks hotlinked images, feeds in readers, CDNs on other domains

Strict-Transport-Security (HSTS)

Directive Meaning
max-age=N Seconds the browser must use HTTPS only for this host. 31536000 is one year. max-age=0 clears the policy.
includeSubDomains Applies the rule to every subdomain too.
preload Signals consent to inclusion in browsers’ built-in HSTS preload list (submitted separately at hstspreload.org). The list’s requirements include a max-age of at least one year, includeSubDomains, and HTTPS on every subdomain.

Roll out in steps: max-age=300, then a week, then a year, checking every subdomain (mail, staging, old apps) works over HTTPS. Browsers ignore the header on plain HTTP responses. Preloading is hard to undo: removal from the list takes a long time to reach users, so treat it as permanent. Send it from the server or CDN, not from WordPress, so it covers every response.

Content-Security-Policy (CSP)

A CSP lists where each type of resource may come from. Anything not allowed is blocked and reported in the browser console.

Directives

Directive Controls Falls back to default-src?
default-src The fallback for the fetch directives below n/a
script-src JavaScript: files, inline scripts, event handlers, eval() Yes
script-src-elem, script-src-attr Script elements and inline event handler attributes separately Fall back to script-src
style-src Stylesheets, inline <style> and style="" attributes Yes
img-src Images and favicons Yes
font-src Web fonts Yes
connect-src fetch(), XHR, WebSocket, beacons (analytics, REST API calls) Yes
media-src Audio and video Yes
frame-src What your page may embed in iframes (YouTube, maps, payment fields) Yes (via child-src)
worker-src Web workers and service workers Yes (via child-src, then script-src)
object-src <object> and <embed> plugins Yes. Set it to 'none'.
manifest-src Web app manifests Yes
base-uri Allowed values of <base href> No
form-action Where forms may submit No
frame-ancestors Who may embed your page in a frame No
upgrade-insecure-requests Rewrites http:// subresource URLs to https://. Helps with mixed content after an HTTPS move. n/a
report-to Names a reporting endpoint defined in the Reporting-Endpoints header n/a
report-uri Older way to give a report URL. Deprecated, but still the only option some browsers support, so send both. n/a

Source expressions

Value Allows
'self' The page’s own origin (same scheme, host and port). Quotes are required for keywords.
'none' Nothing
https://CDN.EXAMPLE.COM One host. https://*.EXAMPLE.COM allows its subdomains.
https:, data:, blob: Any URL with that scheme. data: in script-src defeats the point of CSP.
'unsafe-inline' Inline scripts, styles and event handlers. Removes most of CSP’s XSS protection for scripts.
'unsafe-eval' eval(), new Function() and similar
'unsafe-hashes' Inline event handlers or style attributes that match a listed hash
'wasm-unsafe-eval' WebAssembly compilation without allowing JavaScript eval
'nonce-RANDOM-NONCE' Inline or external scripts carrying nonce="RANDOM-NONCE"
'sha256-BASE64HASH' An inline script or style whose exact content hashes to that value (sha384 and sha512 also work)
'strict-dynamic' Scripts loaded by an already trusted (nonced or hashed) script are trusted too; host allowlists and 'self' are then ignored by supporting browsers
'report-sample' Include the first characters of the blocked code in reports

Nonces and hashes

Topic Detail
Nonce rules A new unguessable value (for example 128 bits, base64) for every response, in both the header and each script tag. A reused nonce is no protection.
Nonces and page caching A cached page serves the same nonce to every visitor, which defeats it. Page caches and CDNs need to rewrite the nonce per response, or use hashes instead.
Hashes Work with caching, but every change to the inline code (even whitespace) changes the hash. Inline scripts with per-page data (such as localized script variables) change often.
'unsafe-inline' fallback When a nonce or hash is present in script-src, browsers that support them ignore 'unsafe-inline', so you can list it as a fallback for very old browsers.
WordPress helpers Since 5.7, wp_get_inline_script_tag() and wp_print_inline_script_tag() build inline script tags, and the wp_inline_script_attributes and wp_script_attributes filters let you add a nonce attribute. Plugins that echo their own script tags bypass these filters.

Report-Only mode and reporting

Reporting-Endpoints: csp-endpoint="https://REPORT-ENDPOINT/csp"
Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self' https://CDN.EXAMPLE.COM; img-src 'self' data: https:; object-src 'none'; base-uri 'self'; frame-ancestors 'self'; report-uri https://REPORT-ENDPOINT/csp; report-to csp-endpoint

Content-Security-Policy-Report-Only blocks nothing; it only reports what would be blocked. Run it on real traffic for a while, adjust the policy until the reports are only noise (browser extensions cause many), then switch to the enforcing header. You can send both headers at once to enforce a loose policy while testing a stricter one.

WordPress caveats for CSP

Area What to expect
Block editor and wp-admin Rely on many inline scripts and styles. A strict script-src usually breaks the admin. Apply the policy to the front end first, or use a separate looser policy for /wp-admin/.
Front-end inline code Core, themes and plugins print inline scripts (localized data, block styles, emoji detection on older versions) and inline style attributes from block settings. Strict style-src without 'unsafe-inline' is rarely practical on block themes.
Page builders Often need 'unsafe-inline' and sometimes 'unsafe-eval' in the editor. Test the builder’s editor, not only the front end.
Embeds and iframes YouTube, Vimeo, maps, social posts and payment fields need their hosts in frame-src, and often script-src and img-src.
Analytics and tag managers Need script-src, connect-src and img-src entries. A tag manager can inject arbitrary scripts, so allowing it widens the policy a lot.
Meta tag CSP A policy in <meta http-equiv> cannot use frame-ancestors, report-uri or sandbox, and there is no meta Report-Only. Use the HTTP header.

Framing, sniffing and referrers

Header Values Notes
X-Content-Type-Options nosniff (the only value) Browsers refuse to treat a response as a script or stylesheet unless its Content-Type says so. Make sure the server sends correct types for .js, .css, .mjs and .json.
X-Frame-Options DENY, SAMEORIGIN ALLOW-FROM is obsolete and ignored by current browsers. WordPress already sends SAMEORIGIN on the login screen and in the admin.
CSP frame-ancestors 'none', 'self', or listed origins The modern replacement. Where both are present, browsers that support frame-ancestors use it and ignore X-Frame-Options. It can allow several specific origins, which X-Frame-Options cannot. Send both for older browsers.

WordPress framing caveats: the Customizer and site editor frame your own site (same origin, so SAMEORIGIN and 'self' are fine). DENY or 'none' breaks them. Sites embedded in another app, a partner portal or a page builder preview on a different domain need that origin listed in frame-ancestors.

Referrer-Policy value Same-origin requests send Cross-origin requests send
no-referrer Nothing Nothing
no-referrer-when-downgrade Full URL Full URL, except nothing from HTTPS to HTTP
origin Origin only Origin only
origin-when-cross-origin Full URL Origin only
same-origin Full URL Nothing
strict-origin Origin only Origin only, nothing from HTTPS to HTTP
strict-origin-when-cross-origin Full URL Origin only, nothing from HTTPS to HTTP. The browser default and the recommended baseline.
unsafe-url Full URL Full URL, even to HTTP. Avoid.

Stricter values (no-referrer, same-origin) can break referral reporting in analytics and affiliate tracking, and some payment or anti-spam services that check the referrer.

Permissions-Policy

Allows or blocks browser features for your page and the frames inside it. Syntax: feature=(allowlist), comma-separated.

Allowlist Meaning
() Blocked everywhere, including your own page
(self) Your origin only
(self "https://CDN.EXAMPLE.COM") Your origin and that origin (origins go in double quotes)
* Any origin

Baseline: Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=(), usb=(). Leave out any feature your site uses. Embeds also need the matching allow attribute on the iframe (YouTube’s embed code asks for features such as autoplay, encrypted-media, picture-in-picture and fullscreen); blocking those features in the header breaks the corresponding controls. Store checkout and maps plugins may need payment or geolocation. The older Feature-Policy header used a different syntax and is superseded.

Cross-origin policies

Header Values Effect and WordPress caveats
Cross-Origin-Opener-Policy unsafe-none (default), same-origin-allow-popups, same-origin, noopener-allow-popups Separates your window from cross-origin windows. same-origin breaks pop-up flows that talk back to the opener: payment buttons, social sign-in, some OAuth connections in plugins. same-origin-allow-popups is safer for sites that open such pop-ups.
Cross-Origin-Embedder-Policy unsafe-none (default), require-corp, credentialless With COOP same-origin, enables cross-origin isolation (needed for SharedArrayBuffer and precise timers). require-corp blocks any cross-origin resource that does not opt in with CORP or CORS, which breaks most third-party embeds, ads and images. Most WordPress sites should leave it unset.
Cross-Origin-Resource-Policy same-site, same-origin, cross-origin Set on a response to say who may load it. Useful for private files and APIs. Site-wide same-origin stops other sites, email clients and feed readers from loading your images, and breaks assets on a CDN domain.

Set-Cookie attributes

Attribute Meaning Recommendation
Secure Only sent over HTTPS Always, on HTTPS sites
HttpOnly Not readable by JavaScript (document.cookie) All session and auth cookies. WordPress sets it on its login cookies.
SameSite=Strict Never sent on cross-site requests, including following a link from another site Sensitive cookies where losing the session on inbound links is acceptable
SameSite=Lax Sent on top-level navigations (clicking a link) but not on cross-site subrequests or POSTs Good default for sessions. Chromium treats cookies without SameSite as Lax.
SameSite=None Sent in all contexts Only for cookies that must work in third-party iframes; requires Secure
Domain Shares the cookie with subdomains Leave it out unless subdomains need the cookie: omitting it is stricter
Path Limits the cookie to a URL path Not a security boundary on its own
Max-Age / Expires Lifetime. Without either, a session cookie (cleared when the browser session ends) Keep auth cookies short-lived
Partitioned Stores a third-party cookie separately per top-level site (CHIPS) For embedded widgets that need state; requires Secure
__Host- prefix Browser only accepts it with Secure, Path=/ and no Domain Strongest binding of a cookie to one host
__Secure- prefix Browser only accepts it with Secure For cookies that need a Domain attribute

WordPress hardening checklist

Setting How Why Risk of breaking things
Disable the file editor define( 'DISALLOW_FILE_EDIT', true ); in wp-config.php A stolen admin login cannot paste code into theme or plugin files from the dashboard None for most sites. DISALLOW_FILE_MODS goes further and also blocks installs and updates from the dashboard, so only use it with another update process.
File permissions Directories 755, files 644, wp-config.php 600 or 640 (or what the host recommends); see CS-10 Other accounts and processes cannot change your files Low. Too strict breaks updates and uploads; ownership matters as much as mode.
Unique salts and keys The eight AUTH_KEY to NONCE_SALT constants; regenerate with wp config shuffle-salts or the WordPress.org salt generator Signs login cookies. New salts invalidate every existing session, which is the standard step after a breach. Logs everyone out. No other effect.
Two-factor authentication A 2FA plugin, required at least for administrators and editors A leaked or guessed password alone is no longer enough Low. Plan a recovery method (backup codes) before enforcing it.
Application passwords Core since 5.6, per user under Users, Profile. Revoke unused ones; disable entirely with add_filter( 'wp_is_application_passwords_available', '__return_false' ); They grant REST API and XML-RPC access that bypasses 2FA and the login form Breaks integrations that use them (mobile apps, automation, some publishing tools)
XML-RPC Block xmlrpc.php at the server (CS-07). The xmlrpc_enabled filter only disables authenticated methods; pingbacks stay on. Brute-force amplification (many passwords per request) and pingback abuse Breaks Jetpack, some mobile and desktop publishing apps and old integrations
REST user endpoint Unauthenticated /wp-json/wp/v2/users lists users who have published posts. Restrict it for logged-out requests with the rest_endpoints filter or a security plugin. Reduces username discovery for brute force. Usernames also leak through author archives and feeds, so treat this as low value on its own. Medium if done for logged-in users too: the block editor and plugins use the endpoint
Login rate limiting A security plugin, the host’s WAF, or fail2ban on a VPS. Core has no limit. Slows password guessing at wp-login.php Low. Can lock out real users behind a shared IP (offices, mobile carriers).
No PHP in uploads Server rule denying .php in wp-content/uploads (CS-07) An uploaded backdoor cannot run Low. A few badly written plugins run PHP from uploads; they break.
Directory listing off Options -Indexes (Apache); Nginx is off unless autoindex on Stops anyone browsing plugin folders and backups None
debug.log not public define( 'WP_DEBUG_LOG', '/PATH/OUTSIDE/WEBROOT/debug.log' ); (a path is accepted since 5.1) and define( 'WP_DEBUG_DISPLAY', false );, or deny debug.log at the server The default wp-content/debug.log is downloadable and can expose paths, queries and personal data None
Database table prefix Set at install time Low value: SQL injection is stopped by fixing the vulnerable code, and an attacker who can run SQL can read the prefix from information_schema Changing it on a live site is risky: option names and usermeta keys contain the prefix and must be renamed too (CS-09)
Updates Minor core releases update automatically by default. Enable plugin and theme auto-updates (core since 5.5) where you trust them; remove unused plugins and themes. Most compromises exploit known, already patched vulnerabilities An update can break a site. Keep backups and use staging for major updates.
Least privilege Give users the lowest role that works; audit administrators (wp user list --role=administrator) Fewer accounts that can install code None
HTTPS for admin define( 'FORCE_SSL_ADMIN', true ); (default on sites whose URL is HTTPS) Admin cookies and passwords never travel over HTTP Low: needs a working certificate

security.txt (RFC 9116)

A plain-text file that tells security researchers how to report a vulnerability to you. Serve it at https://EXAMPLE.COM/.well-known/security.txt over HTTPS with Content-Type: text/plain; charset=utf-8. A redirect from the older /security.txt location is allowed.

Contact: mailto:security@example.com
Contact: https://example.com/security-report
Expires: 2027-11-01T00:00:00.000Z
Preferred-Languages: en, es
Canonical: https://example.com/.well-known/security.txt
Policy: https://example.com/security-policy
Acknowledgments: https://example.com/security-thanks
Field Required? Notes
Contact Yes, one or more A URI, usually mailto: or an https:// form. List in order of preference.
Expires Yes, exactly once Date and time in the RFC 3339 format. The RFC recommends less than a year ahead. An expired file should be treated as stale, so put renewal in your calendar.
Encryption No URI of a key for encrypted reports, such as an OpenPGP key
Acknowledgments No Page thanking researchers
Preferred-Languages No, at most once Comma-separated language tags
Canonical No The URL(s) where this file officially lives
Policy No Your vulnerability disclosure policy
Hiring No Security job listings

Lines starting with # are comments. The file may be signed with an OpenPGP cleartext signature; if so, include a Canonical field. In WordPress, either upload a static file to .well-known/ in the web root or let a plugin serve it.

Common recipes

Goal Steps
Safe first set of headers HSTS with a short max-age, X-Content-Type-Options: nosniff, X-Frame-Options: SAMEORIGIN, Referrer-Policy: strict-origin-when-cross-origin, a minimal Permissions-Policy, and CSP frame-ancestors 'self'; base-uri 'self'; object-src 'none'
Build a real CSP Send a Report-Only policy, browse every template (home, post, shop, checkout, forms, embeds) with DevTools open, collect reports, add the needed sources, enforce, keep reporting on
Fix mixed content after an HTTPS move upgrade-insecure-requests as a stopgap, then wp search-replace 'http://EXAMPLE.COM' 'https://EXAMPLE.COM' --dry-run and the real run
Respond to a breach Reset passwords, shuffle salts, revoke application passwords, audit administrators, check for PHP in uploads and recently changed files (CS-10), update everything
Verify headers curl -sI https://EXAMPLE.COM/ | grep -iE 'strict-transport|content-security|x-frame|x-content-type|referrer|permissions|cross-origin', on a page, a 404, a redirect and an image

Gotchas

Problem Explanation
Headers missing on some URLs Apache without always, the Nginx add_header inheritance rule, or the CDN serving a cached copy from before the change.
Header sent twice with different values Set in the server, a plugin and the CDN. Browsers may combine or reject duplicates; for CSP, every policy is enforced, so the strictest wins.
Editor or Customizer broken after adding headers CSP script or style rules, or X-Frame-Options: DENY. Exclude the admin or relax to SAMEORIGIN and 'self'.
Payment or sign-in pop-up stopped working Cross-Origin-Opener-Policy: same-origin. Use same-origin-allow-popups or remove it.
Embedded videos lost fullscreen Permissions-Policy blocks fullscreen, or CSP frame-src lacks the video host.
Subdomain unreachable after HSTS includeSubDomains forced HTTPS on a subdomain without a certificate. Fix the certificate; lowering max-age only helps browsers that see the new value.
Security scanner grades as targets Online header graders are useful checklists, but a top grade with a policy full of 'unsafe-inline' protects less than it looks. Test what breaks, not just the score.

Need the full fix?

Cheat sheets are quick reference. The field guides walk you through each emergency step by step.

Open the Emergency Room

Search the field manual

What is going wrong?

Press / to search from any page, Esc to close