🛡️
セキュリティヘッダー完全ガイド
セキュリティヘッダーとは
HTTP レスポンスヘッダーにセキュリティ指示を含めることで、ブラウザ側の攻撃緩和を実現する仕組み。
コードのバグをなくすことが理想だが、セキュリティヘッダーは XSS・Clickjacking・情報漏洩などを ブラウザレベルで「被害を減らす」第二の防衛線となる。
ヘッダーなしの場合:
攻撃者が XSS を成功させる → スクリプトが制限なく実行
iframe で埋め込まれる → Clickjacking が成立
HTTP で通信を降格される → MITM 攻撃でデータ窃取
ヘッダーありの場合:
XSS が成功しても → CSP がインラインスクリプトをブロック
iframe に埋め込もうとしても → X-Frame-Options/CSP が拒否
HTTP に降格しようとしても → HSTS が HTTPS を強制
Content-Security-Policy(CSP)
ブラウザが読み込み・実行してよいリソースのソースを宣言するヘッダー。XSS 緩和の主力。
ディレクティブ早見表
| ディレクティブ | 対象 | 推奨設定 |
|---|---|---|
default-src | すべてのリソースのデフォルト | 'self' |
script-src | JavaScript | 'self' 'nonce-{random}' |
style-src | CSS | 'self' 'nonce-{random}' |
img-src | 画像 | 'self' data: https: |
font-src | フォント | 'self' |
connect-src | fetch/XHR/WebSocket | 'self' https://api.example.com |
frame-ancestors | iframe 埋め込みの許可元 | 'none'(Clickjacking 対策) |
base-uri | <base> タグの URL | 'self' |
object-src | Flash・プラグイン | 'none' |
form-action | フォームの送信先 | 'self' |
upgrade-insecure-requests | HTTP → HTTPS に自動昇格 | 設定推奨 |
実装例(Flask)
import secrets
from flask import Flask, g, request
app = Flask(__name__)
@app.before_request
def generate_nonce():
g.csp_nonce = secrets.token_urlsafe(16) # リクエストごとにランダム生成
@app.after_request
def add_security_headers(response):
nonce = g.get('csp_nonce', '')
response.headers['Content-Security-Policy'] = (
f"default-src 'self'; "
f"script-src 'self' 'nonce-{nonce}'; "
f"style-src 'self' 'nonce-{nonce}'; "
f"img-src 'self' data: https:; "
f"font-src 'self'; "
f"connect-src 'self'; "
f"object-src 'none'; "
f"base-uri 'self'; "
f"form-action 'self'; "
f"frame-ancestors 'none'; "
f"upgrade-insecure-requests"
)
return response
<!-- テンプレートで nonce を使用 -->
<script nonce="{{ g.csp_nonce }}">
// このスクリプトだけ CSP が許可する
const userId = {{ current_user.id | tojson }};
</script>
CSP の段階的導入
本番に突然 enforce モードを導入すると既存の JS が壊れる可能性がある。
段階的に導入する:
Step 1: Report-Only で違反を収集
Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-report
Step 2: 違反ログを分析して CSP を調整
Step 3: enforce モードに切り替え
Content-Security-Policy: default-src 'self'; report-uri /csp-report
# CSP 違反レポートの受信エンドポイント
@app.route('/csp-report', methods=['POST'])
def csp_report():
report = request.get_json(force=True)
app.logger.warning(f"CSP Violation: {report.get('csp-report', {})}")
return '', 204
CSP の落とし穴
❌ 'unsafe-inline': インラインスクリプトを許可(XSS を直接許可するのと同じ)
❌ 'unsafe-eval': eval() を許可(動的コード実行)
❌ script-src *: 任意の外部スクリプトを許可
❌ data: を script-src に: data:text/javascript,... で実行可能
❌ JSONP エンドポイントのドメインを許可: そのドメイン経由で任意 JS を実行可能
例: script-src https://cdnjs.cloudflare.com
→ https://cdnjs.cloudflare.com/angular.min.js を読み込んで
Angular テンプレートインジェクションで CSP をバイパスできる
(known JSONP/Angular bypassable ドメインに注意)
Strict-Transport-Security(HSTS)
ブラウザに「このドメインは常に HTTPS で接続せよ」と指示するヘッダー。MITM 攻撃・プロトコルダウングレード攻撃を防ぐ。
HTTP でアクセス → 301 Redirect → HTTPS
HSTS があると:
2回目以降のアクセスはブラウザが自動で HTTPS にアップグレード
→ HTTP リクエスト自体が送られない(傍受できない)
@app.after_request
def add_hsts(response):
# max-age: 1年(秒)推奨
# includeSubDomains: サブドメインにも適用
# preload: ブラウザの HSTS プリロードリストに登録申請可能にする
response.headers['Strict-Transport-Security'] = (
'max-age=31536000; includeSubDomains; preload'
)
return response
注意事項:
・HTTPS が正しく設定されてから有効にする(HTTP しか使えない状態で設定するとサイトが開けなくなる)
・includeSubDomains を設定する前に全サブドメインが HTTPS 対応していることを確認
・HSTS プリロード: hstspreload.org に登録するとブラウザのハードコードリストに入る
→ ブラウザが初回から HTTP リクエストを送らなくなる(最強の設定)
X-Content-Type-Options
ブラウザが Content-Type を無視してコンテンツを推測する「MIME スニッフィング」を禁止する。
MIME スニッフィング攻撃:
攻撃者がテキストファイルに偽装した JavaScript をアップロード
サーバーが Content-Type: text/plain で返す
ブラウザがファイルの中身から「これは JS だ」と推測して実行してしまう
対策:
X-Content-Type-Options: nosniff
→ ブラウザは Content-Type を絶対に信頼し、推測しない
response.headers['X-Content-Type-Options'] = 'nosniff'
X-Frame-Options(Legacy)
このページを iframe で埋め込むことを制御する。Clickjacking 対策。
# DENY: すべての iframe 埋め込みを禁止(推奨)
# SAMEORIGIN: 同一オリジンの iframe のみ許可
response.headers['X-Frame-Options'] = 'DENY'
X-Frame-Options は古いヘッダー。
現代の推奨: CSP の frame-ancestors ディレクティブを使う。
・より細かい制御が可能(複数オリジンの許可など)
・両方設定しておくと後方互換性も担保できる
Referrer-Policy
リンクを踏んだときに Referer ヘッダーに何を含めるかを制御する。
デフォルト動作:
https://example.com/secret-page → https://external.com/
Referer: https://example.com/secret-page ← URL が漏れる
対策: パス情報を隠す
| 値 | 動作 |
|---|---|
no-referrer | Referer ヘッダーを送らない |
no-referrer-when-downgrade | HTTPS→HTTP のとき送らない(デフォルト) |
same-origin | 同一オリジンへのリクエストのみ送る |
strict-origin | オリジン(スキーム+ドメイン)のみ送る |
strict-origin-when-cross-origin | 同一オリジン時は full URL、クロスオリジン時はオリジンのみ(推奨) |
response.headers['Referrer-Policy'] = 'strict-origin-when-cross-origin'
Permissions-Policy(旧 Feature-Policy)
ブラウザの機能(カメラ・マイク・位置情報等)へのアクセスを制御する。
response.headers['Permissions-Policy'] = (
'camera=(), ' # カメラ: 全て禁止
'microphone=(), ' # マイク: 全て禁止
'geolocation=(), ' # 位置情報: 全て禁止
'payment=self, ' # 決済: 同一オリジンのみ
'fullscreen=self' # フルスクリーン: 同一オリジンのみ
)
XSS が成功した場合でも、Permissions-Policy があればカメラ・マイクへのアクセスを防げる。
サードパーティ iframe に対しても機能を制限できる。
Cache-Control(機密ページのキャッシュ制御)
# 機密情報を含むページ・API レスポンスはキャッシュしない
@app.route('/api/v1/users/me')
@jwt_required
def get_profile():
response = jsonify(current_user.to_dict())
response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate'
response.headers['Pragma'] = 'no-cache' # HTTP/1.0 互換
return response
no-store: キャッシュに保存しない(最も強力)
no-cache: 再検証なしでキャッシュを使わない
must-revalidate: 期限切れキャッシュを再検証なしで使わない
機密ページに no-store を設定しないと:
・ブラウザの「戻る」ボタンでログアウト後も内容を見られる
・共有端末でキャッシュから個人情報が漏れる
Cross-Origin ヘッダー群(CORP・COEP・COOP)
高度なブラウザ分離機能。SharedArrayBuffer・Spectre 対策として必要。
# Cross-Origin Resource Policy: 他オリジンからの読み込みを制御
response.headers['Cross-Origin-Resource-Policy'] = 'same-origin'
# same-origin: 同一オリジンのみ読み込み可
# same-site: 同一サイト(サブドメイン含む)まで許可
# cross-origin: 全て許可(デフォルト)
# Cross-Origin Embedder Policy: 全サブリソースに CORP または CORS を要求
response.headers['Cross-Origin-Embedder-Policy'] = 'require-corp'
# Cross-Origin Opener Policy: クロスオリジンとのブラウジングコンテキスト共有を制限
response.headers['Cross-Origin-Opener-Policy'] = 'same-origin'
# この3つを設定すると SharedArrayBuffer を使えるようになる(Spectre 対策済み環境として)
Nginx / Caddy での一括設定
# Nginx: セキュリティヘッダーをまとめて設定
server {
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
# CSP はアプリ側で nonce を生成するため、ここでは基本設定のみ
# add_header Content-Security-Policy "default-src 'self'; object-src 'none';" always;
}
# Caddy: セキュリティヘッダー(Caddyfile)
example.com {
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
X-Content-Type-Options "nosniff"
X-Frame-Options "DENY"
Referrer-Policy "strict-origin-when-cross-origin"
Permissions-Policy "camera=(), microphone=(), geolocation=()"
-Server # Server ヘッダーを削除(バージョン情報の非開示)
}
}
ヘッダーのスコアリングと評価ツール
securityheaders.com: URL を入力するとヘッダーをスキャンして A〜F のグレードで評価
各ヘッダーの優先度:
Critical(必須):
✅ Content-Security-Policy
✅ Strict-Transport-Security
✅ X-Content-Type-Options
Important(強く推奨):
✅ X-Frame-Options(または CSP frame-ancestors)
✅ Referrer-Policy
Recommended(推奨):
✅ Permissions-Policy
✅ Cross-Origin-Resource-Policy
セキュリティヘッダー チェックリスト
□ CSP を設定し 'unsafe-inline' / 'unsafe-eval' を使っていない
□ CSP に nonce を使いリクエストごとにランダム生成している
□ CSP の Report-Only で違反を収集・モニタリングしている
□ Strict-Transport-Security を max-age=31536000; includeSubDomains で設定している
□ X-Content-Type-Options: nosniff を設定している
□ X-Frame-Options: DENY または CSP frame-ancestors 'none' を設定している
□ Referrer-Policy を strict-origin-when-cross-origin に設定している
□ Permissions-Policy でカメラ・マイク・位置情報を制限している
□ 機密 API レスポンスに Cache-Control: no-store を設定している
□ Server / X-Powered-By ヘッダーを削除してバージョン情報を非開示にしている
□ securityheaders.com でスキャンして A グレード以上を確認している
参考文献
- MDN Web Docs『HTTP headers』— 各ヘッダーの仕様・ブラウザサポート表(developer.mozilla.org)
- OWASP Secure Headers Project(owasp.org/www-project-secure-headers)— 全ヘッダーの推奨値と設定例
- W3C CSP Level 3 仕様(w3.org)— Content-Security-Policy の正式仕様
- RFC 6797『HTTP Strict Transport Security(HSTS)』— HSTS の設計と実装仕様
- securityheaders.com — ヘッダー設定をスキャンして評価するオンラインツール
出典: MDN Web Docs Security / OWASP Secure Headers Project / securityheaders.com / RFC 6797(HSTS)/ W3C CSP Level 3