.htaccess Generator for WordPress

Security Headers Guide

JA

What Are Security Headers?

Security headers are HTTP response headers that a server attaches to its responses to the browser. The browser reads these headers and automatically applies various security behaviors — such as restricting script execution, enforcing HTTPS communication, and blocking iframe embedding.

Because they can be applied without modifying application-layer code (PHP or JavaScript), they are widely used as a server-level layer of defense through .htaccess .

Benefits of configuring security headers

  • No code changes required — You can defend against many attacks with server configuration alone, without touching plugin or theme code.
  • Reliably activates browser security features — Modern browsers have many built-in security features, but many of them are not enabled automatically unless instructed by a header.
  • Improves security scan ratings — Scores improve in assessment tools such as Mozilla Observatory and Security Headers, demonstrating the trustworthiness of your site.
  • Limits the blast radius of WordPress plugin vulnerabilities — Even if a plugin with an XSS vulnerability is exploited, CSP and X-Content-Type-Options act as a second line of defense.

Caveats and drawbacks

  • Misconfiguration can break your site — CSP in particular can block JavaScript, CSS, or images if the policy is too strict. Always test with Report-Only mode first.
  • May conflict with plugins — If a WordPress plugin already outputs security headers on its own, they may duplicate with .htaccess . Disable the plugin's header output or consolidate to one source.
  • HSTS is difficult to undo once set — Once cached by the browser, reverting to HTTP requires gradually reducing max-age and waiting for the cache to expire.

Headers configurable in the generator

Header Primary threat defended against
Strict-Transport-Security (HSTS) HTTP→HTTPS downgrade attacks / man-in-the-middle (MITM)
Content-Security-Policy (CSP) XSS / Mixed Content / clickjacking
X-Content-Type-Options XSS via MIME sniffing
X-Frame-Options Clickjacking
Referrer-Policy Leakage of sensitive information contained in URLs
Permissions-Policy Unauthorized use of device APIs (camera, microphone, etc.)

All of these headers are output through Apache's mod_headers module. Wrapping them in <IfModule mod_headers.c> prevents 500 errors in environments where the module is not enabled.


Header set vs Header always set

Directive Scope
Header set Success responses (2xx) only
Header always set Applied to error responses (403 / 404, etc.) as well

Always use Header always set for security headers. The always keyword is required so the browser receives security instructions even on error pages.


HSTS (HTTP Strict Transport Security)

Header always set Strict-Transport-Security \
  "max-age=63072000; includeSubDomains; preload" \
  "expr=%{HTTPS} == 'on' || %{HTTP:X-Forwarded-Proto} == 'https'"

Role

This header declares to the browser: "Always connect to this site over HTTPS from now on."

An HTTPS redirect in .htaccess works only after the request reaches the server. HSTS upgrades the connection on the browser side before the request is even sent, eliminating the risk of man-in-the-middle (MITM) attacks .

What each parameter means

Parameter Meaning
max-age=63072000 How long the browser should cache the HSTS policy (in seconds). 63,072,000 s = 2 years
includeSubDomains Apply HSTS to all subdomains as well
preload Signal intent to be added to browser HSTS preload lists (enforces HTTPS even on the very first visit)

The role of the expr= condition (reverse-proxy support)

Shared hosting services such as XServer and ConoHa WING use a Nginx → Apache reverse-proxy architecture. In this setup, even when a user connects over HTTPS, Apache receives the request over plain HTTP internally, so %{HTTPS} alone cannot detect the original protocol.

User →(HTTPS)→ Nginx (TLS termination) →(HTTP)→ Apache

The expr= condition uses Apache's Expression evaluation engine to send the header only when either an HTTPS connection or an X-Forwarded-Proto: https header is detected. This prevents the HSTS header from being sent on plain-HTTP requests.

Before enabling includeSubDomains , verify that every subdomain already supports HTTPS. Any subdomain that does not support HTTPS will become inaccessible.

The generator lets you toggle includeSubDomains and preload independently. Turn off includeSubDomains if any subdomain lacks HTTPS, and turn off preload if you do not intend to register with a preload list.


CSP (Content Security Policy)

Header always set Content-Security-Policy \
  "upgrade-insecure-requests; \
   script-src 'self' 'unsafe-inline' 'unsafe-eval'; \
   style-src 'self' 'unsafe-inline'; \
   img-src 'self' data:; \
   frame-src https://www.youtube.com https://www.google.com; \
   frame-ancestors 'self'"

The role of upgrade-insecure-requests

This directive instructs the browser to automatically upgrade http:// resources (images, CSS, JS, etc.) on the page to https:// . It is used to automatically fix Mixed Content pages (pages that mix HTTP and HTTPS resources).

In the generator, enabling CSP always includes upgrade-insecure-requests in the output (it cannot be removed). Individual directives can then be toggled on or off independently.

Directives configurable in the generator

Directive What it restricts Default value
default-src Fallback for any directive not explicitly specified 'self'
script-src Allowed origins for JavaScript 'self'
style-src Allowed origins for CSS 'self'
img-src Allowed origins for images 'self' data:
font-src Allowed origins for fonts 'self'
connect-src Allowed destinations for Ajax / Fetch / WebSocket 'self'
frame-src External URLs that can be loaded in iframes. Shortcuts for YouTube / Google Maps available. 'none'
frame-ancestors Parent URLs that are allowed to embed this page in an iframe (supersedes X-Frame-Options) 'self'

WordPress-specific considerations

WordPress core, the Gutenberg block editor, and admin-panel plugins rely heavily on inline scripts, inline styles, and eval() , so 'unsafe-inline' / 'unsafe-eval' are required in the wp-admin area.

When CSP is enabled, the generator automatically produces separate CSP directives for wp-admin / wp-login.php and the front end. The front end is hardened without unsafe-* , while the admin area receives a CSP that includes the necessary unsafe-* values, so both can coexist.

The img-src / OG image pitfall

Explicitly specifying img-src disables the default-src fallback for images. Watch out for this common mistake:

# WRONG: forgetting 'self' blocks images on your own site
img-src data:

# CORRECT: always include 'self'
img-src 'self' data:

OG images (thumbnails used when sharing on social media) are also subject to this rule. Always include 'self' whenever you write img-src .

Admin-area CSP branching (WordPress)

When CSP is enabled in the generator, it automatically outputs different CSP directives for the front end and for wp-admin / wp-login.php. The front end excludes unsafe-* from script-src / style-src, while the admin area gets a CSP with the unsafe-inline / unsafe-eval values that WordPress requires pre-applied.

# Front end (everything except wp-admin and wp-login.php)
<If "%{REQUEST_URI} !~ m#^/wp-(admin(?:/|$)|login\.php)#">
    Header always set Content-Security-Policy "upgrade-insecure-requests; ..."
</If>

# Admin panel and login page
<If "%{REQUEST_URI} =~ m#^/wp-(admin(?:/|$)|login\.php)#">
    Header always set Content-Security-Policy "upgrade-insecure-requests; default-src 'self' https:; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' https:; frame-ancestors 'self'"
</If>

The admin-area CSP is generated dynamically based on the directives the user has selected. 'unsafe-inline' and 'unsafe-eval' are added automatically to script-src , and 'unsafe-inline' is added to style-src , ensuring that the Gutenberg block editor and core admin screens function correctly.

Even if script-src / style-src are not explicitly enabled, if default-src is enabled the admin-area CSP will inherit its value and add explicit script-src / style-src entries.

Report-Only mode

Applying CSP directly to a production site can break it. The safe approach is to first use the Content-Security-Policy-Report-Only header to collect violation reports, verify them, and then switch to enforcing mode.

# Test mode (reports violations without actually blocking anything)
Header always set Content-Security-Policy-Report-Only "default-src 'self'; report-uri /csp-report"

upgrade-insecure-requests is ignored in Report-Only mode. Because it is an action directive (upgrading resources), not a restriction directive, it should only be included in an enforcing CSP.

Checking violations in DevTools

With the Report-Only header in place, open your site and go to the browser DevTools (F12) → Console tab. Any CSP violations will appear as errors like this:

Refused to load the script 'https://www.googletagmanager.com/gtm.js'
because it violates the following Content Security Policy directive:
"script-src 'self'"

Review the violations in the Console, add the required origins to the appropriate directives, and then switch to Enforce mode ( Content-Security-Policy ).

Collecting violation reports (report-uri / report-to)

CSP violation reports can be sent via the traditional report-uri directive, and in CSP Level 3 also via report-to and Reporting-Endpoints (or the older Report-To header for compatibility). Since report-uri is deprecated but still widely supported, combining both is the most practical approach for collecting real-user violations that DevTools alone cannot capture.

Header always set Reporting-Endpoints "csp-endpoint=\"/wp-json/csp-report/v1/collect\""
Header always set Report-To "{\"group\":\"csp-endpoint\",\"max_age\":10886400,\"endpoints\":[{\"url\":\"https://example.com/wp-json/csp-report/v1/collect\"}]}"
Header always set Content-Security-Policy-Report-Only \
  "default-src 'self'; report-uri /wp-json/csp-report/v1/collect; report-to csp-endpoint"

Reporting-Endpoints is the modern syntax, while Report-To is the older Reporting API header. Because browser support differs, include both if compatibility is important. The long-term direction is to migrate to the report-to approach, but combining it with report-uri is the safe choice for now.

Example: implementing a report-uri receiver endpoint in the WordPress REST API:

The report-to (Reporting API) format differs — it uses application/reports+json , an array, and a type: "csp-violation" field — and requires a separate receiver implementation. The example below is for report-uri only.

add_action( 'rest_api_init', function () {
    register_rest_route( 'csp-report/v1', '/collect', array(
        'methods'             => 'POST',
        'callback'            => function ( WP_REST_Request $request ) {
            // Validate Content-Type (report-uri sends application/csp-report or application/json)
            $content_type = (string) $request->get_header( 'Content-Type' );
            if ( false === strpos( $content_type, 'application/csp-report' )
                && false === strpos( $content_type, 'application/json' ) ) {
                return new WP_REST_Response( null, 415 );
            }
            // Enforce body size limit (4 KB)
            $body = $request->get_body();
            if ( strlen( $body ) > 4096 ) {
                return new WP_REST_Response( null, 413 );
            }
            $data   = json_decode( $body, true );
            $report = isset( $data['csp-report'] ) ? $data['csp-report'] : array();
            // Save only the fields needed (exclude document-uri / referrer — they may contain PII)
            $log = array(
                'blocked-uri'         => isset( $report['blocked-uri'] ) ? $report['blocked-uri'] : '',
                'violated-directive'  => isset( $report['violated-directive'] ) ? $report['violated-directive'] : '',
                'effective-directive' => isset( $report['effective-directive'] ) ? $report['effective-directive'] : '',
            );
            error_log( 'CSP violation: ' . wp_json_encode( $log ) );
            return new WP_REST_Response( null, 204 );
        },
        'permission_callback' => '__return_true',
    ) );
} );

Anyone can POST to a CSP report endpoint. Saving reports without validation can lead to log poisoning, log bloat (DoS), and recording of personally identifiable information (PII) such as URLs and Referer values. In production, avoid persisting fields that may contain PII (e.g. document-uri / referrer ), and always enforce body-size limits and Content-Type validation. Delegating to an external service is also a valid option when a high volume of reports is expected.

Using an external service

Instead of hosting your own endpoint, you can delegate report collection to a SaaS service.

Service Notes
report-uri.com Collects and visualizes CSP, CT, and HPKP reports. Free plan available.
Sentry Error-monitoring platform that also accepts CSP reports.

X-Content-Type-Options

Header always set X-Content-Type-Options "nosniff"

Role and threat model

This header disables the browser's ability to automatically guess (sniff) the MIME type of a response.

One known attack that exploits MIME sniffing involves uploading a JavaScript file disguised as an image and tricking the browser into executing it as a script (XSS). Specifying nosniff forces the browser to strictly respect the Content-Type header value returned by the server, preventing this attack.


X-Frame-Options

Header always set X-Frame-Options "SAMEORIGIN"

Role and threat model

This header restricts other sites from embedding your pages inside an <iframe> , protecting against clickjacking attacks.

Clickjacking is an attack technique where an attacker overlays your site in a transparent iframe on their own page, tricking users into clicking invisible buttons or links.

Directive values

Value Meaning
SAMEORIGIN Allow embedding from the same origin only (recommended)
DENY Block all embedding

The generator lets you choose between SAMEORIGIN (default) and DENY . If your site never embeds its own pages in iframes, DENY provides stricter protection.


Referrer-Policy

Header always set Referrer-Policy "strict-origin-when-cross-origin"

Role

Controls how much information the browser includes in the Referer header when a user follows a link to another site. Because URLs may contain admin paths or personal information, this header prevents that data from leaking to external sites.

Available policies

The generator lets you choose from the following 8 policies. The default is strict-origin-when-cross-origin (recommended).

Policy Behavior
no-referrer Never send a referrer
no-referrer-when-downgrade No referrer on HTTPS→HTTP downgrade; full URL otherwise
origin Always send the origin (domain) only
origin-when-cross-origin Full URL for same-origin; origin only for cross-origin
same-origin Full URL for same-origin only; nothing for cross-origin
strict-origin Origin only; nothing on downgrade
strict-origin-when-cross-origin Full URL for same-origin; origin only for cross-origin; nothing on downgrade ( recommended )
unsafe-url Always send the full URL ( not recommended — path information leaks to external sites)

strict-origin-when-cross-origin behavior details

Destination What is sent
Same origin (within your site) Complete URL including path
External origin (https→https) Origin only ( https://example.com )
External origin (https→http) Nothing

Permissions-Policy

Header always set Permissions-Policy \
  "camera=(), microphone=(), payment=(), usb=(), gyroscope=(), magnetometer=(), geolocation=()"

Role

Restricts access to powerful browser APIs (camera, microphone, payment, etc.). Disabling device APIs that a typical WordPress site doesn't need prevents malware or XSS attacks from calling them without authorization.

Empty parentheses () completely disable that feature — no origin is allowed to use it.

Configurable features

The generator lets you toggle each feature individually. Enabling a feature outputs feature=() to restrict it; disabling it excludes the feature from the policy (the browser default applies). The only exception is geolocation , which offers three levels instead of a simple on/off: "fully disabled / allow Google Maps / exclude from policy".

Feature Description When to disable restriction
camera Camera API When using video calls or QR code readers
microphone Microphone API When using voice input or calling features
payment Payment Request API E-commerce sites using Web Payment
usb WebUSB API When communication with USB devices is required
gyroscope Gyroscope API When device rotation detection is needed
magnetometer Magnetometer API When compass functionality is required
geolocation Geolocation API When displaying maps or getting current location (e.g. Google Maps)

geolocation — 3-level setting

The generator provides three options for geolocation :

Option Generated value Use case
Fully disabled geolocation=() Sites that don't use maps or current-location features (default)
Allow Google Maps geolocation=(self "https://www.google.com") Sites that use the Google Maps "show my location" feature
Exclude from policy (nothing output) Leave to browser default

If you embed Google Maps but don't use the "show my location" button, "Fully disabled" is fine. Only choose "Allow Google Maps" when the current-location feature is actually needed.


Defense map — coverage of each header

┌── HSTS ──────────── Enforce HTTPS at the browser level
├── CSP ───────────── Auto-fix Mixed Content + restrict resources
├── X-Content-Type ── Block XSS via MIME-type spoofing
├── X-Frame-Options ─ Prevent clickjacking
├── Referrer-Policy ─ Control URL leakage
└── Permissions ───── Block unauthorized use of device APIs

All of these headers are output as response headers via the mod_headers module. Wrapping them in <IfModule mod_headers.c> prevents 500 errors in environments where the module is disabled.

<IfModule mod_headers.c>
    Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" "expr=%{HTTPS} == 'on' || %{HTTP:X-Forwarded-Proto} == 'https'"
    Header always set Content-Security-Policy "upgrade-insecure-requests; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; frame-src https://www.youtube.com https://www.google.com; frame-ancestors 'self'"
    Header always set X-Content-Type-Options "nosniff"
    Header always set X-Frame-Options "SAMEORIGIN"
    Header always set Referrer-Policy "strict-origin-when-cross-origin"
    Header always set Permissions-Policy "camera=(), microphone=(), payment=(), usb=(), gyroscope=(), magnetometer=(), geolocation=()"
</IfModule>
← Back to Generator