HTTP security headers, CSP & cookie flags
SkillSecurityUse when adding HTTP response headers to a WordPress site or plugin: Content-Security-Policy (or Report-Only), X-Content-Type-Options, frame protection (X-Frame-Options or frame-ancestors), Referrer-Policy, Permissions-Policy, HSTS, Secure/HttpOnly/SameSite cookie flags, or CORS on REST responses. Covers the wp_headers filter, the send_headers, login_init and admin_init surfaces, per-request CSP nonces via script_loader_tag, and REST origin restriction through core's allowlist. Headers are the second XSS layer after output escaping, and they also stop clickjacking and MIME sniffing.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the HTTP security headers, CSP & cookie flags skill
What this skill tells your AI
The instructions your AI receives, as published by wpultimatesecurity/wordpress-security-skills in skills/security-headers-csp/SKILL.md and read by ahel’s review.
When to use this skill
Use this skill whenever code touches HTTP responses on any WordPress surface:
- Adding response headers (front-end, wp-admin, or wp-login.php).
- Building a Content-Security-Policy, migrating Report-Only to enforced, or wiring script nonces.
- Choosing frame protection without breaking the Customizer preview or oEmbed frames.
- Setting Secure / HttpOnly / SameSite on a custom cookie, or forcing Secure on core auth cookies.
- Deciding what REST responses may reflect into Access-Control-Allow-Origin.
- Reviewing code that calls
header(), reads$_SERVER['HTTP_ORIGIN'], or filterswp_headers.
Response headers are the second layer. Output escaping prevents XSS; headers contain the blast radius when escaping was missed somewhere, and they stop whole attack classes escaping cannot: clickjacking, MIME sniffing, and inline script injection. Agents rarely add headers. When they do, they tend to add a blanket CSP that admins disable, or CORS wide open.
Related: see the output-escaping skill for the primary XSS defense, the
rest-api-security skill for endpoint authorization, and the
wp-hardening-best-practices skill for server-layer TLS and .htaccess config.
Core principles (and why they matter)
- Headers mitigate; escaping prevents. A CSP is not an XSS fix. It limits what an injected script can load and whether it runs at all. Ship both.
- Three surfaces, one policy. Front-end template loads fire
send_headersand thewp_headersfilter. wp-login.php and wp-admin do not run the main query, so they never reach that code: uselogin_initfor the login screen andadmin_initfor admin, both before output, with one shared function callingheader().login_headfires inside the login page<head>, after HTML output has started:header()there is a silent no-op. It is only good for<meta http-equiv>fallbacks. - Frame control must permit same-origin framing. The Customizer and theme
previews iframe the front-end from the same origin; oEmbed previews frame
posts.
X-Frame-Options: SAMEORIGIN(or CSPframe-ancestors 'self') is the safe default. Blanket DENY breaks the Customizer preview. - CSP ships incrementally. Start with
Content-Security-Policy-Report-Only, triage violation reports, then enforce. Enforcing a strict policy on day one breaks the site and teaches admins to delete your headers. - A CSP nonce is not a CSRF nonce. Generate it fresh per response with
random_bytes()+bin2hex().wp_create_nonce()returns a CSRF token: user-bound, reused across requests for the session window, and designed to be printed into the page. Wrong tool forscript-src 'nonce-...'. - Inline scripts do not inherit the nonce.
wp_add_inline_script()output is printed bywp_get_inline_script_tag(), not through yourscript_loader_tagfilter. Nonce it via thewp_inline_script_attributesfilter. - Restrict CORS with core's allowlist, never reflection. Core's
rest_send_cors_headers()reflects the requestOriginintoAccess-Control-Allow-Origintogether withAccess-Control-Allow-Credentials: true, and does not consult the allowlist. Filterhttp_originso disallowed origins produce no CORS headers at all; extend the allowlist withallowed_http_origins. Never sendAccess-Control-Allow-Origin: *together with credentials. - Cookie flags are cheap and mandatory. Every custom cookie:
Secure,HttpOnly,SameSite, via the PHP 7.3+ options array. Core auth cookies already sendHttpOnly(hardcoded inwp_set_auth_cookie(), noauth_cookie_httponlyhook exists); the filterable part isSecureviasecure_auth_cookie/secure_logged_in_cookie. - HSTS is a domain commitment. Send it only from an HTTPS-only site: once cached, a broken certificate makes the domain unreachable. Server-layer config (nginx / .htaccess) is the better home.
- Do not strip headers you did not add (for example, removing core's frame protection) without a documented reason and a replacement.
Step-by-step implementation
-
Add the baseline to the front-end through
wp_headers(full module:references/secure-security-headers.php):add_filter( 'wp_headers', 'myplugin_front_end_security_headers' ); function myplugin_front_end_security_headers( $headers ) { $headers['X-Content-Type-Options'] = 'nosniff'; $headers['Referrer-Policy'] = 'strict-origin-when-cross-origin'; $headers['X-Frame-Options'] = 'SAMEORIGIN'; return $headers; } -
Cover the other two surfaces with one shared function hooked to
login_initandadmin_init; it callsheader( 'Header: value' )per header, guarded byheaders_sent(). -
Decide frame policy:
SAMEORIGIN/frame-ancestors 'self'unless the site is documented to never be framed. Do not use DENY site-wide. -
Write the CSP and ship it as
Content-Security-Policy-Report-Onlyfirst. Buildscript-srcaround a per-response nonce:function myplugin_csp_nonce() { static $nonce = null; if ( null === $nonce ) { $nonce = bin2hex( random_bytes( 16 ) ); } return $nonce; } -
Attach the nonce to enqueued scripts through
script_loader_tag, limited to your own handles. Noncewp_add_inline_script()output throughwp_inline_script_attributes. If you enforcestyle-src, do the same viastyle_loader_tag. -
Watch the Report-Only console/violation output until it is quiet, then rename the header to
Content-Security-Policy. -
Set cookie flags with the PHP 7.3+ options array. On HTTPS-only sites, force
Secureon core auth cookies withsecure_auth_cookie. -
Restrict REST CORS: filter
http_originto return an empty string unless the origin is inget_allowed_http_origins(); add partner origins through theallowed_http_originsfilter. -
Verify:
curl -sIone URL per surface, plus the browser console for CSP violations. Re-check after enabling full-page caching: a cache that freezes HTML freezes the per-response nonce.
Supporting references
| Reference | Load when |
|---|---|
| HTTP security headers checklist | Before final verification of the http security headers controls. |
| HTTP headers cheat sheet | Choosing the applicable WordPress API or control for HTTP headers and CSP. |
| Secure HTTP security headers | Implementing baseline headers across WordPress surfaces, Report-Only CSP nonces, cookie flags, and REST origin restrictions. |
Common AI mistakes / anti-patterns
Mistake 1 - CSP theater
// ❌ Insecure: permits everything; any inline or remote script runs. Pure theater.
header( 'Content-Security-Policy: script-src * unsafe-inline' );
// ✅ Secure: self plus a per-response nonce; observed in Report-Only before enforcing.
header( "Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-" . myplugin_csp_nonce() . "'" );
Mistake 2 - Enforcing without ever running Report-Only
// ❌ Insecure: strict enforced CSP from day one breaks theme/plugin assets;
// the admin's first fix is deleting your header.
header( "Content-Security-Policy: default-src 'self'" );
// ✅ Secure: identical policy, observed only. Enforce after violations are triaged.
header( "Content-Security-Policy-Report-Only: default-src 'self'" );
Mistake 3 - Reflecting Origin with credentials
// ❌ Insecure: any site can read logged-in users' responses; reflection plus
// credentials is a full cross-origin grant to everyone.
header( 'Access-Control-Allow-Origin: ' . $_SERVER['HTTP_ORIGIN'] );
header( 'Access-Control-Allow-Credentials: true' );
// ✅ Secure: keep core's header logic, and restrict what it may reflect via the
// http_origin filter. Empty origin means core sends no CORS headers.
add_filter( 'http_origin', 'myplugin_restrict_http_origin' );
function myplugin_restrict_http_origin( $origin ) {
if ( '' !== $origin && ! in_array( $origin, get_allowed_http_origins(), true ) ) {
return '';
}
return $origin;
}
Mistake 4 - Headers on the front-end only
// ❌ Insecure: login and admin get nothing; the login form is a favorite
// framing and injection target.
add_filter( 'wp_headers', 'myplugin_front_end_security_headers' ); // and nothing else
// ✅ Secure: one shared function, three surfaces.
add_action( 'login_init', 'myplugin_login_security_headers' );
add_action( 'admin_init', 'myplugin_admin_security_headers' );
Mistake 5 - Using wp_create_nonce() as a CSP nonce
// ❌ Insecure: a CSRF token is user-bound and reused across requests; it is
// also printed into forms anyway. Leakage stays valid for the session window.
$nonce = wp_create_nonce( 'csp' );
header( "Content-Security-Policy: script-src 'self' 'nonce-$nonce'" );
// ✅ Secure: fresh unpredictable value per response.
header( "Content-Security-Policy: script-src 'self' 'nonce-" . bin2hex( random_bytes( 16 ) ) . "'" );
Mistake 6 - Blanket X-Frame-Options: DENY
// ❌ Insecure: DENY forbids same-origin framing; the Customizer preview and
// oEmbed previews stop working.
$headers['X-Frame-Options'] = 'DENY';
// ✅ Secure: allow same-origin framing only.
$headers['X-Frame-Options'] = 'SAMEORIGIN';
// Or in the CSP: frame-ancestors 'self'
Mistake 7 - SameSite=None without Secure
// ❌ Insecure: browsers reject SameSite=None over insecure contexts; the
// cookie silently disappears, or ships cross-site without TLS.
setcookie( 'myplugin_session', $token, array( 'samesite' => 'None' ) );
// ✅ Secure: None only together with Secure; default to Lax otherwise.
setcookie(
'myplugin_session',
$token,
array(
'expires' => 0,
'path' => '/',
'secure' => true,
'httponly' => true,
'samesite' => 'None',
)
);
Correct code examples
The complete module (all three surfaces, CSP Report-Only with nonce pipeline,
custom cookie helper, HTTPS-only auth cookies, REST origin restriction) is in
references/secure-security-headers.php.
Per-header values, what each header stops, and caveats are in
references/headers-cheatsheet.md.
Pre-ship review list: references/checklist.md.
Checklist
- Front-end headers set via
wp_headers(orsend_headers), not scatteredheader()calls in templates. - wp-login.php covered via
login_init(notlogin_head: that fires after output starts); wp-admin viaadmin_init. - Frame protection is
SAMEORIGIN/frame-ancestors 'self', not site-wide DENY. - CSP ran as
Content-Security-Policy-Report-Onlyfirst; violations triaged before enforcement. - CSP nonce generated per response with
random_bytes()+bin2hex(). - Enqueued scripts nonced only for plugin handles via
script_loader_tag. - Inline scripts nonced via
wp_inline_script_attributes. - Full-page caching behavior checked: no frozen nonce in cached HTML.
- No
Access-Control-Allow-Origin: *combined withAccess-Control-Allow-Credentials: true; REST origins restricted through core's allowlist filters. - Custom cookies set
Secure,HttpOnly,SameSite;SameSite=Noneonly withSecure. - Core auth cookie
Secureforced viasecure_auth_cookie/secure_logged_in_cookieon HTTPS-only sites. - HSTS sent only on HTTPS-only sites, or configured at the server layer.
- No pre-existing headers removed without a documented reason.
- Verified per surface with
curl -sIand the browser CSP console.
Official references
wp_headersfiltersend_headersactionlogin_initactionlogin_headactionadmin_initactionscript_loader_tagfilterstyle_loader_tagfilterwp_inline_script_attributesfilterwp_add_inline_script()wp_enqueue_script()wp_get_inline_script_tag()nocache_headers()rest_send_cors_headers()get_http_origin()is_allowed_http_origin()get_allowed_http_origins()http_originfilterallowed_http_originsfiltersecure_auth_cookiefiltersecure_logged_in_cookiefilterwp_set_auth_cookie()- OWASP HTTP Headers Cheat Sheet
- MDN: Content Security Policy (CSP)
- MDN: CSP frame-ancestors
Signals
- GitHub stars
- 31
- Forks
- 2
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
security-headers-csp- Source
- github.com/wpultimatesecurity/wordpress-security-skills