セキュリティヘッダーとは
セキュリティヘッダーは、サーバーがブラウザへのレスポンスに付与する HTTP ヘッダーのこと。ブラウザはこのヘッダーを読み取り、スクリプトの実行制限・通信の強制 HTTPS 化・iframe 埋め込みのブロックなど、様々なセキュリティ動作を自動的に行う。
アプリケーション側のコード(PHP・JavaScript)を変更せずに適用できるため、.htaccessによるサーバーレベルでの防御として広く使われる。
設定するメリット
- コード変更不要で防御を追加できる — プラグインやテーマのコードに手を入れずに、サーバー設定だけで多くの攻撃を防げる。
- ブラウザの安全機能を確実に有効化できる — 現代のブラウザはセキュリティ機能を多数持っているが、ヘッダーが指示しないと自動で有効にならないものが多い。
- 脆弱性スキャンの評価が上がる — セキュリティ診断ツール(例: Mozilla Observatory、Security Headers)でのスコアが向上し、サイトの信頼性をアピールできる。
- WordPress のプラグイン脆弱性の影響を局限できる — XSS 脆弱性を持つプラグインが攻撃されても、CSP や X-Content-Type-Options が第二の防御線として機能する。
注意点・デメリット
- 設定ミスでサイトが壊れる可能性がある — 特に CSP は設定が厳しすぎると JavaScript・CSS・画像が読み込めなくなる。まず Report-Only モードでテストするのが安全。
-
プラグインとの競合が起きる場合がある
— WordPress プラグインがセキュリティヘッダーを独自に出力している場合、
.htaccessと二重になる。プラグイン側の設定を無効化するか、どちらかに統一する。 -
HSTS は一度設定すると取り消しが難しい
— ブラウザにキャッシュされるため、HTTP 運用に戻したい場合は
max-ageを短くしてから段階的に解除する必要がある。
ジェネレーターで設定できるヘッダー一覧
| ヘッダー名 | 主な防御対象 |
|---|---|
Strict-Transport-Security
(HSTS)
|
HTTP→HTTPS ダウングレード攻撃・中間者攻撃(MITM) |
Content-Security-Policy
(CSP)
|
XSS・Mixed Content・クリックジャッキング |
X-Content-Type-Options |
MIME スニッフィングを悪用した XSS |
X-Frame-Options |
クリックジャッキング |
Referrer-Policy |
URL に含まれる機密情報の外部漏洩 |
Permissions-Policy |
カメラ・マイク等のデバイス API の不正利用 |
これらのヘッダーはすべて Apache の
mod_headersモジュールで出力する。<IfModule mod_headers.c>でラップすることで、モジュールが無効な環境でも 500 エラーを防げる。
Header set vs Header always set
| ディレクティブ | 適用範囲 |
|---|---|
Header set |
成功レスポンス(200系)のみ |
Header always set |
エラーレスポンス(403 / 404 等)にも付与 |
セキュリティヘッダーは
Header always setを使う。エラーページでもブラウザに安全な動作を指示するため、alwaysが必須。
HSTS(HTTP Strict Transport Security)
Header always set Strict-Transport-Security \
"max-age=63072000; includeSubDomains; preload" \
"expr=%{HTTPS} == 'on' || %{HTTP:X-Forwarded-Proto} == 'https'"
役割
ブラウザに「今後必ず HTTPS で接続してください」と宣言するヘッダー。
.htaccess の HTTPS リダイレクトはリクエストがサーバーに到達してからリダイレクトを行う。HSTS はブラウザ側で事前に HTTPS 化するため、 中間者攻撃(MITM)のリスクをゼロ にできる。
各パラメータの意味
| パラメータ | 意味 |
|---|---|
max-age=63072000 |
HSTS ポリシーを保持する期間(秒)。63072000 秒 = 2年 |
includeSubDomains |
サブドメインにも HSTS を適用する |
preload |
ブラウザの HSTS プリロードリストへの登録を許可する(初回アクセスも HTTPS 化) |
expr= 条件の役割(Nginx リバースプロキシ対応)
XServer・ConoHa WING などのレンタルサーバーは Nginx → Apache のリバースプロキシ構成を採用している。この場合、ユーザーが HTTPS でアクセスしてもサーバーの Apache は HTTP で受け取るため、
%{HTTPS}
だけでは判定できない。
ユーザー →(HTTPS)→ Nginx(TLS終端) →(HTTP)→ Apache
expr=
条件は Apache の式(Expression)評価エンジンを使い、HTTPS 接続または
X-Forwarded-Proto: https
のどちらかが成立する場合のみヘッダーを送信する。これにより HTTP アクセス時に HSTS ヘッダーが送られることを防ぐ。
includeSubDomainsを有効にする前に、すべてのサブドメインが HTTPS に対応していることを確認する。対応していないサブドメインにアクセスできなくなる。
ジェネレーターでは
includeSubDomains
と
preload
をそれぞれ個別に ON/OFF できる。サブドメインが HTTPS 非対応の場合は
includeSubDomains
を外す、プリロードリスト登録が不要なら
preload
を外す、といった調整が可能。
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'"
upgrade-insecure-requests の役割
ページ内の
http://
リソース(画像・CSS・JS など)を自動的に
https://
に変換する指示。Mixed Content(HTTP と HTTPS が混在するページ)を自動修正するために使う。
ジェネレーターでは CSP を有効にすると
upgrade-insecure-requests
は常に出力される(削除不可)。その上で各ディレクティブを個別に ON/OFF できる。
ジェネレーターで設定できるディレクティブ
| ディレクティブ | 制限対象 | デフォルト値 |
|---|---|---|
default-src |
他のディレクティブのフォールバック(指定がない場合に適用) | 'self' |
script-src |
JavaScript の読み込み元 | 'self' |
style-src |
CSS の読み込み元 | 'self' |
img-src |
画像の読み込み元 | 'self' data: |
font-src |
フォントの読み込み元 | 'self' |
connect-src |
Ajax / Fetch / WebSocket の接続先 | 'self' |
frame-src |
iframe で読み込める外部 URL。YouTube / Google Maps のショートカットあり | 'none' |
frame-ancestors |
このページを iframe に埋め込める親 URL(X-Frame-Options の上位互換) | 'self' |
WordPress 固有の注意点
WordPress のコアや Gutenberg ブロックエディター、管理画面プラグインは多数のインラインスクリプト・インラインスタイルや
eval()
を使用するため、wp-admin では
'unsafe-inline'
/
'unsafe-eval'
が必要になる。
ジェネレーターは CSP 有効時に wp-admin / wp-login.php とフロントエンドを自動的に分岐出力する。フロントエンドは
unsafe-*
なしで厳格化しつつ、管理画面には必要な
unsafe-*
を付与した CSP を別途適用するため、両立が可能。
img-src と OG image の落とし穴
img-src
を明示すると
default-src
のフォールバックが効かなくなる。以下のミスに注意する。
# NG: 'self' を入れ忘れると自サイト画像もブロックされる
img-src data:
# OK: 必ず 'self' を含める
img-src 'self' data:
OG image(SNS シェア時に使われるサムネイル画像)もブロック対象になる。
img-srcを書くなら必ず'self'を含める。
管理画面用 CSP 分岐(WordPress)
ジェネレーターで CSP を有効にすると、フロントエンドと wp-admin / wp-login.php で自動的に異なる CSP を出力する。フロントエンドは script-src / style-src から unsafe-* を除外し、wp-admin にはあらかじめ unsafe-inline / unsafe-eval を許可した CSP が適用される。
# フロントエンド(wp-admin・wp-login.php 以外)
<If "%{REQUEST_URI} !~ m#^/wp-(admin(?:/|$)|login\.php)#">
Header always set Content-Security-Policy "upgrade-insecure-requests; ..."
</If>
# 管理画面・ログインページ
<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>
管理画面側の CSP はユーザーが選択したディレクティブをベースに動的生成される。
script-src
には自動的に
'unsafe-inline'
と
'unsafe-eval'
が、
style-src
には
'unsafe-inline'
のみが付与され、Gutenberg ブロックエディターやコアの管理画面が正常動作する。
script-src/style-srcを有効にしていない場合でも、default-srcが有効なら管理画面 CSP はその値を継承してscript-src/style-srcを明示的に追加する。
Report-Only モード
いきなり本番環境に CSP を適用するとサイトが壊れるリスクがある。まず
Content-Security-Policy-Report-Only
ヘッダーでテストし、違反レポートを確認してから本番適用するのが安全。
# テスト用(実際のブロックは行わず、違反をレポートのみ)
Header always set Content-Security-Policy-Report-Only "default-src 'self'; report-uri /csp-report"
Report-Only モードでは
upgrade-insecure-requestsは無視される。これはアクション指示(リソースのアップグレード)であり、制限指示ではないため。本番 CSP にのみ含める。
DevTools で違反を確認する
Report-Only ヘッダーを設定した状態でサイトを表示し、ブラウザの DevTools(F12)→ Console タブを開く。CSP 違反が発生すると以下のようなエラーが出力される。
Refused to load the script 'https://www.googletagmanager.com/gtm.js'
because it violates the following Content Security Policy directive:
"script-src 'self'"
Console に出力された違反を確認し、許可すべきオリジンを各ディレクティブに追加してから Enforce モード(
Content-Security-Policy
)へ移行する。
違反レポートを収集する(report-uri / report-to)
CSP 違反レポートは、従来の
report-uri
に加えて、CSP Level 3 では
report-to
と
Reporting-Endpoints
(互換目的では
Report-To
)でも送信できる。
report-uri
は現在も多くのブラウザで動作するが非推奨のため、DevTools だけでは把握しきれない実ユーザー環境での違反を収集する場合は新旧両方を併記するのが現実的。
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は新しい書き方で、Report-Toは旧来の Reporting API ヘッダー。対応状況に差があるため、互換性を重視するなら両方を併記する。将来的にはreport-to系への移行を前提にしつつ、当面はreport-uriと併用するのが安全。
WordPress REST API で
report-uri
向け受信エンドポイントを実装する例:
report-to(Reporting API)はボディ形式が異なり(application/reports+json・配列 +type: "csp-violation")、受信処理を別途実装する必要がある。以下の例はreport-uri専用。
add_action( 'rest_api_init', function () {
register_rest_route( 'csp-report/v1', '/collect', array(
'methods' => 'POST',
'callback' => function ( WP_REST_Request $request ) {
// Content-Type 検証(report-uri: application/csp-report または 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 );
}
// ボディサイズ上限(4KB)
$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();
// 必要なフィールドのみ保存(document-uri / referrer は 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',
) );
} );
CSP レポートは第三者が任意に POST できる。そのまま保存するとログ汚染・ログ肥大化(DoS)や URL・Referer などの個人情報(PII)の記録につながる。本番運用では
document-uri/referrerなど PII を含むフィールドの保存を避け、ボディサイズ上限・Content-Type 検証を必ず実施すること。大量のレポートが予想される場合は外部サービスへの委任も有効。
外部サービスを使う
自前でエンドポイントを用意せず、SaaS サービスに収集させる方法もある。
| サービス | 特徴 |
|---|---|
| report-uri.com | CSP・CT・HPKP レポートを収集・可視化。無料プランあり |
| Sentry | エラー監視ツール。CSP レポートも受け付ける |
X-Content-Type-Options
Header always set X-Content-Type-Options "nosniff"
役割と防御対象
ブラウザが MIME タイプを自動推測(MIME スニッフィング)する機能を無効にするヘッダー。
MIME スニッフィングを悪用した攻撃として、画像ファイルに偽装した JavaScript をアップロードしてブラウザに実行させる XSS 攻撃がある。
nosniff
を指定すると、サーバーが返す
Content-Type
ヘッダーの値をブラウザが厳格に遵守するため、この攻撃を防げる。
X-Frame-Options
Header always set X-Frame-Options "SAMEORIGIN"
役割と防御対象
他サイトがこのサイトのページを<iframe>
で埋め込むことを制限するヘッダー。クリックジャッキング攻撃を防止する。
クリックジャッキングとは、攻撃者が自分のサイトに対象サイトを透明な
iframe
で重ね、ユーザーに見えないボタンをクリックさせる攻撃手法。
設定値の意味
| 値 | 意味 |
|---|---|
SAMEORIGIN |
同一オリジンからの埋め込みのみ許可(推奨) |
DENY |
すべての埋め込みを禁止 |
ジェネレーターでは
SAMEORIGIN
(デフォルト)と
DENY
を選択できる。自サイトで iframe 埋め込みが不要なら
DENY
を選ぶとより厳格になる。
Referrer-Policy
Header always set Referrer-Policy "strict-origin-when-cross-origin"
役割
リンクを踏んで他サイトに移動したとき、ブラウザが
Referer
ヘッダーにどれだけの情報を含めるかを制御するヘッダー。URL には管理画面のパスや個人情報を含む場合があり、外部サイトへの漏洩を防ぐ。
選択可能なポリシー一覧
ジェネレーターでは以下の 8 つのポリシーから選択できる。デフォルトは
strict-origin-when-cross-origin
(推奨)。
| ポリシー | 動作 |
|---|---|
no-referrer |
リファラーを一切送信しない |
no-referrer-when-downgrade |
HTTPS→HTTP ダウングレード時のみ非送信。それ以外はフル URL を送信 |
origin |
常にオリジン(ドメイン)のみ送信 |
origin-when-cross-origin |
同一オリジンはフル URL、クロスオリジンはオリジンのみ |
same-origin |
同一オリジンのみフル URL を送信。クロスオリジンは非送信 |
strict-origin |
オリジンのみ送信。ダウングレード時は非送信 |
strict-origin-when-cross-origin |
同一オリジンはフル URL、クロスオリジンはオリジンのみ、ダウングレード時は非送信( 推奨 ) |
unsafe-url |
常にフル URL を送信( 非推奨 — パス情報が外部に漏洩する) |
strict-origin-when-cross-origin の動作詳細
| 移動先 | 送信内容 |
|---|---|
| 同一オリジン(自サイト内) | 完全な URL(パスを含む)を送信 |
| 外部オリジン(https→https) |
オリジン部分のみ送信(
https://example.com
)
|
| 外部オリジン(https→http) | 何も送信しない |
Permissions-Policy
Header always set Permissions-Policy \
"camera=(), microphone=(), payment=(), usb=(), gyroscope=(), magnetometer=(), geolocation=()"
役割
ブラウザが持つ強力な API(カメラ・マイク・決済など)の使用を制限するヘッダー。WordPress サイトで通常使用しないデバイス API を無効化することで、マルウェアや XSS 攻撃が不正にこれらの API を呼び出すことを防ぐ。
空の括弧
()
はそのディレクティブを完全に無効化することを意味する(すべてのオリジンからの使用を禁止)。
制御可能な機能一覧
ジェネレーターでは以下の機能を個別に ON/OFF できる。チェック ON で制限(
feature=()
を出力)、OFF でポリシーから除外(ブラウザデフォルトで許可)。なお
geolocation
のみ例外で、ON/OFF の 2 択ではなく「完全無効化 / Google マップ許可 / ポリシーから除外」の 3 段階で設定できる。
| 機能 | 説明 | OFF にするケース |
|---|---|---|
camera |
カメラ API | ビデオ通話・QR コードリーダー等を使う場合 |
microphone |
マイク API | 音声入力・通話機能を使う場合 |
payment |
Payment Request API | Web Payment を使う EC サイト |
usb |
WebUSB API | USB デバイスとの通信が必要な場合 |
gyroscope |
ジャイロスコープ API | デバイスの回転検知が必要な場合 |
magnetometer |
磁力センサー API | コンパス機能が必要な場合 |
geolocation |
位置情報 API | Google マップ等の地図表示・現在地取得を使う場合 |
geolocation の 3 段階設定
geolocation
はジェネレーターで以下の 3 択から選択できる。
| 選択肢 | 生成される値 | 用途 |
|---|---|---|
| 完全無効化 | geolocation=() |
地図・現在地機能を一切使わないサイト(デフォルト) |
| Google マップ許可 |
geolocation=(self "https://www.google.com")
|
Google マップの「現在地を表示」機能を使うサイト |
| ポリシーから除外 | (出力なし) | ブラウザデフォルトに委ねる場合 |
Google マップを埋め込んでいても「現在地を表示」ボタンを使わないサイトなら「完全無効化」のままで問題ない。「現在地を表示」が必要な場合のみ「Google マップ許可」を選ぶ。
防御マップ — 各ヘッダーの担当範囲
┌── HSTS ──────────── HTTP→HTTPS 強制(ブラウザレベル)
├── CSP ───────────── Mixed Content 自動修正 + リソース制限
├── X-Content-Type ── MIME 偽装による XSS 防止
├── X-Frame-Options ─ クリックジャッキング防止
├── Referrer-Policy ─ URL 情報の漏洩制御
└── Permissions ───── デバイス API の不正利用防止
これらのヘッダーはすべてレスポンスヘッダーとして
mod_headers
モジュールで出力する。
<IfModule mod_headers.c>
でラップすることで、モジュールが無効な環境でも 500 エラーを防げる。
<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>