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-ageand 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_headersmodule. 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 setfor security headers. Thealwayskeyword 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 writeimg-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-srcare not explicitly enabled, ifdefault-srcis enabled the admin-area CSP will inherit its value and add explicitscript-src/style-srcentries.
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-requestsis 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-Endpointsis the modern syntax, whileReport-Tois the older Reporting API header. Because browser support differs, include both if compatibility is important. The long-term direction is to migrate to thereport-toapproach, but combining it withreport-uriis 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 usesapplication/reports+json, an array, and atype: "csp-violation"field — and requires a separate receiver implementation. The example below is forreport-urionly.
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>