How to harden security headers (CSP, HSTS) from PHP in WordPress

Contents

Introduction

This is a comprehensive, implementation-ready tutorial for hardening HTTP security headers for WordPress sites using PHP. It covers HSTS (Strict-Transport-Security) and CSP (Content-Security-Policy) end-to-end: concepts, risks, best practices, rollout strategy, WordPress-specific implementation patterns, nonce-based CSP for inline scripts, reporting, server-level notes, testing and operational caveats. All code examples are ready to drop in functions.php or a small plugin.

Why security headers matter

HTTP response headers are one of the most effective and low-cost ways to reduce risk from a range of attacks: TLS downgrade, mixed content, clickjacking, cross-site scripting (XSS) echoes, resource exfiltration, and more. HSTS forces browsers to speak only HTTPS to your host. CSP significantly reduces XSS impact by restricting the origins and types of allowed content and can enforce other policies (frame-ancestors, form-action, base-uri).

Threats mitigated

  • HSTS prevents SSL stripping and accidental plaintext access after a user loads a site over HTTPS.
  • CSP reduces XSS attack surface (scripts, styles, images, frames, etc.), mitigates data exfiltration via script-controlled network requests, and can prevent clickjacking via frame-ancestors.

High-level recommendations

  1. Serve your site only over HTTPS before enabling HSTS. HSTS applied to an insecure site will lock users out of HTTP access unexpectedly.
  2. Roll out CSP with Report-Only first to gather violations and fix legitimate resource loads before switching to enforce mode.
  3. Prefer a single, well-structured Content-Security-Policy header per response. Avoid a permissive policy like unsafe-inline where possible.
  4. Use nonces or hashes for inline scripts/styles when you must include inline code, and add nonces to enqueued scripts and inline blocks generated by WordPress.
  5. Be careful with HSTS preloading: only request inclusion after you understand the implications and after all subdomains support HTTPS.

HSTS (Strict-Transport-Security)

Header format and flags

Syntax:

Strict-Transport-Security: max-age=31536000 includeSubDomains preload

max-age: time in seconds. Use at least 31536000 (1 year) for production when ready. For initial testing use a short time (e.g., 86400).
includeSubDomains: causes the header to apply to all subdomains. Only set if every subdomain supports HTTPS.
preload: signals intent to include the site on the HSTS preload list (https://hstspreload.org/). Do not add until you meet preload requirements.

Precautions

  • Do not enable HSTS for a domain that is not fully HTTPS across all subdomains if you use includeSubDomains.
  • Once browsers receive HSTS for the domain, they will refuse HTTP for the max-age — roll back requires waiting for max-age to expire unless you can update responses to reset max-age to 0 over HTTPS.
  • Preload is irreversible without a lengthy removal process from the preload list — be conservative.

Setting HSTS from PHP / WordPress

You can set HSTS either via server configuration (preferred for performance) or from PHP. In WordPress, send headers early (via the send_headers action or the wp_headers filter) so headers are present before output.

Minimal PHP example (set only when HTTPS is active):


Better: detect forwarded protocol when behind a proxy/load-balancer (respect X-Forwarded-Proto in a trusted environment):


Server-level HSTS (preferred)

If you control your webserver, configure HSTS at the server level. This avoids PHP overhead and guarantees headers before PHP runs.

nginx example:

add_header Strict-Transport-Security max-age=31536000 includeSubDomains preload always

Apache example (.htaccess or virtual host):


    Header always set Strict-Transport-Security max-age=31536000 includeSubDomains preload

Content-Security-Policy (CSP)

Overview of CSP

CSP is a powerful, granular policy language. It lets you declare which origins and options are allowed for scripts, styles, images, frames, web workers, fonts, connections (XHR, fetch, websockets), and more. Use CSP to:

  • Prevent execution of injected scripts
  • Control what origins may be contacted by scripts (connect-src)
  • Prevent framing/clickjacking (frame-ancestors)
  • Block mixed content (block-all-mixed-content/upgrade-insecure-requests)

Key directives (non-exhaustive)

  • default-src
  • script-src, style-src, img-src, font-src, connect-src, media-src
  • frame-ancestors — controls which sites can embed your page
  • base-uri, form-action — limit allowed base URIs and form posting
  • object-src — usually set to none to block plugins
  • block-all-mixed-content, upgrade-insecure-requests
  • report-uri or report-to (report only) — send violation reports to a collector

Nonce- and hash-based inline script handling

Inline scripts/styles are common in WordPress (theme templates, plugins). To avoid unsafe-inline, prefer:

  • Nonces: unique per response (cryptographically random). Add nonce attribute to each inline script/style tag (e.g., nonce=randomnonce), and include nonce-randomnonce in script-src/style-src.
  • Hashes: hash the exact inline code (sha256-…). Best for fixed inline blocks that never change.

Nonces are often easiest in WordPress: generate one per request, add it to the CSP header, then ensure enqueued scripts and inline blocks have the nonce attribute. Use script_loader_tag filter to add attributes to enqueued scripts.

How multiple CSP headers are handled

Browsers will process multiple Content-Security-Policy headers as if the directives are combined. That can lead to unexpected permissive behavior if different headers allow different things. Its best to send a single, well-controlled CSP header.

CSP rollout strategy

  1. Start with a tight but non-enforcing policy using Content-Security-Policy-Report-Only and collect reports.
  2. Fix legitimate violations (plugins/themes/external resources).
  3. Convert to enforcement by sending Content-Security-Policy.
  4. Continue monitoring via reporting endpoints.

WordPress implementation patterns

1) Basic static CSP header (quick and dirty)

This sets a broad CSP that allows self for most content and a couple of common external resources. Use Report-Only during testing.


2) Generate and use a per-request nonce

A robust approach: create a nonce for each request, add it into the CSP, and attach the nonce attribute to script/style tags for both enqueued and inline items.

Nonce generation and header injection (functions.php or plugin):


3) Add the nonce to enqueued scripts inline scripts

Use the script_loader_tag filter to inject the nonce attribute into every script tag that is enqueued by WordPress. For inline script blocks that are printed directly in templates, use a helper function to print the nonce attribute.

script_loader_tag filter example:


    tag = str_replace(
?>

For inline scripts added via wp_add_inline_script(), WordPress will output them as separate