security-headers について
security-headersは、ミドルウェアでWebアプリにブラウザー向けセキュリティヘッダーを設定する方法を解説する開発者向けスキルです。CSP、クリックジャッキングやMIME対策、本番環境のHSTS、非公開ルートのインデックス制御を扱います。
主な機能
- 01Content-Security-Policyの設定
- 02X-Frame-Optionsによるクリックジャッキング対策
- 03nosniffによるMIME対策
- 04本番環境のみのHSTS
- 05ダッシュボードルートのインデックス抑制
活用シーン
- 01ミドルウェアでWebリクエストにヘッダーを付与
- 02CSPで必要なサードパーティドメインを許可
- 03デプロイ後のセキュリティヘッダーを確認
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ harperaa/ secure-claude-skills --skill security-headersインストール後、Claude Code で/security-headersを入力して読み込まれたか確認します。
詳細
セキュリティヘッダー - 複数の攻撃からの防御
セキュリティヘッダーが重要な理由
セキュリティヘッダーは、城を囲む壁と堀のようなものです。攻撃者が門(認証)を突破しても、壁(ヘッダー)が自由な移動やデータの持ち出しを防ぎます。
ブラウザーのセキュリティモデル
最新のブラウザーにはセキュリティ機能が組み込まれていますが、明示的に有効化する必要があります。適切なヘッダーがないと、ブラウザーは次のことを許可します。
- 悪意のある iframe にサイトが埋め込まれる(クリックジャッキング)
- あらゆるオリジンからのスクリプト実行(XSS の増幅)
- 安全でない HTTP 接続(中間者攻撃)
- MIME タイプの混同(画像をスクリプトとして実行)
セキュリティヘッダーは、ブラウザーに「このサイトでは、すべてのセキュリティ機能を有効にしてください」と伝えます。
ヘッダーがない場合の実際の影響
Scott Helme が 2023 年に実施した、上位 10,000 件の Web サイトを対象とするセキュリティ監査によると、推奨されるすべてのセキュリティヘッダーを適切に実装しているサイトはわずか 2.8% です。残りの 97.2% は、ヘッダーによって防げる攻撃に対して脆弱です。
Magecart 攻撃(2018~2020 年): 何百もの EC サイトが、決済情報を盗む JavaScript を埋め込まれて侵害されました。Content-Security-Policy ヘッダーがあれば、これらのスクリプトの実行を防げました。CSP を設定していないサイトでは、不正な取引によって数百万ドルの損失が発生しました。
セキュリティヘッダーの構成
すべてのヘッダーは、各リクエストに対して middleware.ts により自動的に適用されます。手動で設定する必要はありません。すでに保護されています。
適用するヘッダー
- Content-Security-Policy (CSP) - リソースの読み込みを制御します
- X-Frame-Options: DENY - クリックジャッキングを防ぎます
- X-Content-Type-Options: nosniff - MIME タイプの混同を防ぎます
- Strict-Transport-Security (HSTS) - HTTPS を強制します(本番環境のみ)
- X-Robots-Tag: noindex, nofollow - 非公開ルートを保護します
ヘッダーの説明と実装
1. Content-Security-Policy (CSP)
機能: 読み込み可能なリソース(スクリプト、スタイル、画像)と、その読み込み元を制御します。
設定内容:
// Dynamic CSP based on environment variables
const clerkDomain = process.env.NEXT_PUBLIC_CLERK_FRONTEND_API_URL
? new URL(process.env.NEXT_PUBLIC_CLERK_FRONTEND_API_URL).origin
: '';
const convexDomain = process.env.NEXT_PUBLIC_CONVEX_URL
? new URL(process.env.NEXT_PUBLIC_CONVEX_URL).origin
: '';
const csp = [
"default-src 'self'",
`script-src 'self' 'unsafe-inline' 'unsafe-eval' ${clerkDomain} https://js.stripe.com`,
`style-src 'self' 'unsafe-inline' ${clerkDomain}`,
`connect-src 'self' ${clerkDomain} ${convexDomain} https://api.stripe.com`,
`frame-src 'self' ${clerkDomain} https://js.stripe.com https://hooks.stripe.com`,
"img-src 'self' data: https: blob:",
"font-src 'self' data:",
"object-src 'none'",
"base-uri 'self'",
"form-action 'self'",
"frame-ancestors 'none'"
].join('; ');
response.headers.set('Content-Security-Policy', csp);
この設定の意味:
- スクリプト: 自社ドメイン、Clerk、Stripe からのみ許可します
- スタイル: 自社ドメインと Clerk からのみ許可します
- 接続: Clerk、Convex、Stripe の API へのみ許可します
- フレーム: Clerk と Stripe のみ許可します
- 画像: HTTPS のあらゆるソースを許可します(ユーザーのアバターや外部画像用)
動的な設定を採用する理由:
const clerkDomain = process.env.NEXT_PUBLIC_CLERK_FRONTEND_API_URL
? new URL(process.env.NEXT_PUBLIC_CLERK_FRONTEND_API_URL).origin
: ''
Clerk のドメインをコードに固定していません。環境変数から取得します。これにより、次のことが可能になります。
- ✅ 開発/ステージング/本番環境ごとに異なるドメインを自動的に使用できます
- ✅ コードを変更せずに簡単に変更できます
- ✅ コードベースにセキュリティ上の秘密情報を含めずに済みます
トレードオフ: スクリプトに対して unsafe-inline と unsafe-eval を許可しています。これは Next.js と Clerk の動作に必要です。入力のサニタイズと検証によって、このリスクを軽減しています。
データの持ち出しを防ぐ:
connect-src ディレクティブにより、悪意のあるスクリプトが許可されていないドメインにデータを送信するのを防ぎます。XSS がサニタイズをすり抜けた場合でも、ブラウザーが許可されていないネットワークリクエストをブロックします。
2. X-Frame-Options: DENY
防止する攻撃: 攻撃者のサイトにある透明な iframe にサイトを埋め込むクリックジャッキング攻撃です。
攻撃シナリオ: 攻撃者は、ゲームサイト上に透明な iframe を重ね、その中に「アカウントを削除」ボタンを埋め込みます。ユーザーは「ゲームをプレイ」をクリックしているつもりでも、実際には「アカウントを削除」をクリックしてしまいます。
保護の仕組み:
response.headers.set('X-Frame-Options', 'DENY');
DENY を指定すると、ブラウザーは自社のサイトであっても、あらゆる iframe へのサイトの埋め込みを拒否します。これにより、最大限のセキュリティを確保できます。
代替値:
DENY- iframe への埋め込みを一切許可しません(最も安全な設定。ここで使用しています)SAMEORIGIN- 自社のサイトからの iframe のみ許可しますALLOW-FROM uri- 非推奨のため、使用しないでください
3. X-Content-Type-Options: nosniff
防止する攻撃: ブラウザーが画像を JavaScript として実行する MIME 混同攻撃です。
攻撃シナリオ: 攻撃者が JavaScript を含む「avatar.jpg」というファイルをアップロードします。古いブラウザーは「親切心」からファイルの種類を「スニッフィング」し、JavaScript を検出して実行します。
このプロジェクトの保護対策:
response.headers.set('X-Content-Type-Options', 'nosniff');
nosniff は、Content-Type ヘッダーに厳密に従い、推測しないようブラウザーに指示します。
重要な理由: このヘッダーがないと、攻撃者は次のことができます。
<script>evil()</script>を含む「image.jpg」をアップロードする<img src="/uploads/image.jpg">でリンクする- ブラウザーがファイルをスニッフィングしてスクリプトを検出し、実行する
- アップロード時の検証をすり抜けて XSS が成立する
nosniff があると、ブラウザーは Content-Type: image/jpeg を認識し、スクリプトとしての実行を拒否します。
4. Strict-Transport-Security(HSTS)- 本番環境のみ
防止する攻撃: 中間者が HTTPS を HTTP にダウングレードする、SSL ストリッピング攻撃です。
攻撃シナリオ: ユーザーが「yourapp.com」(https:// なし)と入力します。ブラウザーは最初に HTTP でリクエストします。攻撃者がそれを傍受し、偽の HTTP バージョンを提供して認証情報を盗みます。
このプロジェクトの保護対策:
if (process.env.NODE_ENV === 'production') {
response.headers.set(
'Strict-Transport-Security',
'max-age=31536000; includeSubDomains'
);
}
設定の内訳:
max-age=31536000- 有効期間は1年ですincludeSubDomains- すべてのサブドメインに適用されます- 一度設定すると、ブラウザーはドメインへの接続にHTTPS のみを使用します
本番環境のみで有効にする理由: 開発環境では localhost(HTTP)を使用します。HSTS を有効にするとローカルでの開発に支障が出ます。このミドルウェアは環境を検出し、本番環境でのみ HSTS を有効にします。
重要: ドメインに HSTS を一度設定すると、ブラウザーはその設定を記憶します。解除する必要がある場合は、次の手順が必要です。
max-age=0を設定する- すべてのユーザーのブラウザーが新しいヘッダーを受信するまで待つ
- max-age が長かった場合、数か月かかることがあります
5. X-Robots-Tag: noindex, nofollow(保護されたルート)
防止する攻撃: 検索エンジンによる非公開コンテンツのインデックス登録です。
重要な理由:
/dashboard/payment-details が Google の検索結果に表示されるのは望ましくありません。
実装:
if (req.nextUrl.pathname.startsWith('/dashboard')) {
response.headers.set('X-Robots-Tag', 'noindex, nofollow');
}
適用対象: /dashboard/* ルートのみ(公開ページはインデックス登録できるようにします)
検索エンジンへの指示:
noindex- このページを検索結果に追加しないnofollow- このページのリンクをたどらない
ミドルウェアでの実装
middleware.ts のパターン
import { clerkMiddleware } from '@clerk/nextjs/server';
import { NextResponse } from 'next/server';
export default clerkMiddleware((auth, req) => {
const response = NextResponse.next();
// Get dynamic domains from environment
const clerkDomain = process.env.NEXT_PUBLIC_CLERK_FRONTEND_API_URL
? new URL(process.env.NEXT_PUBLIC_CLERK_FRONTEND_API_URL).origin
: '';
const convexDomain = process.env.NEXT_PUBLIC_CONVEX_URL
? new URL(process.env.NEXT_PUBLIC_CONVEX_URL).origin
: '';
// Build CSP dynamically
const csp = [
"default-src 'self'",
`script-src 'self' 'unsafe-inline' 'unsafe-eval' ${clerkDomain} https://js.stripe.com`,
`style-src 'self' 'unsafe-inline' ${clerkDomain}`,
`connect-src 'self' ${clerkDomain} ${convexDomain} https://api.stripe.com`,
`frame-src 'self' ${clerkDomain} https://js.stripe.com https://hooks.stripe.com`,
"img-src 'self' data: https: blob:",
"font-src 'self' data:",
"object-src 'none'",
"base-uri 'self'",
"form-action 'self'",
"frame-ancestors 'none'"
].join('; ');
// Apply security headers
response.headers.set('Content-Security-Policy', csp);
response.headers.set('X-Content-Type-Options', 'nosniff');
response.headers.set('X-Frame-Options', 'DENY');
// HSTS only in production
if (process.env.NODE_ENV === 'production') {
response.headers.set(
'Strict-Transport-Security',
'max-age=31536000; includeSubDomains'
);
}
// Prevent indexing of protected routes
if (req.nextUrl.pathname.startsWith('/dashboard')) {
response.headers.set('X-Robots-Tag', 'noindex, nofollow');
}
return response;
});
export const config = {
matcher: ['/((?!.*\\..*|_next).*)', '/', '/(api|trpc)(.*)'],
};
セキュリティヘッダーのテスト
すべてのヘッダーをテストする
curl -I http://localhost:3000
# Expected headers:
# X-Frame-Options: DENY
# X-Content-Type-Options: nosniff
# Content-Security-Policy: default-src 'self'; ...
本番環境の HSTS をテストする
# In production
curl -I https://yourapp.com
# Should include:
# Strict-Transport-Security: max-age=31536000; includeSubDomains
保護されたルートのヘッダーをテストする
curl -I http://localhost:3000/dashboard
# Should include:
# X-Robots-Tag: noindex, nofollow
オンラインでのヘッダーテスト
デプロイ済みのサイトをテストするには、次のオンラインツールを使用してください。
- Security Headers: https://securityheaders.com/
- Mozilla Observatory: https://observatory.mozilla.org/
- SSL Labs: https://www.ssllabs.com/ssltest/
新しい連携に合わせた CSP のカスタマイズ
新しいサードパーティーサービスを追加する場合:
例:Google Analytics の追加
// middleware.ts
// Add Google Analytics domain to CSP
const csp = [
"default-src 'self'",
`script-src 'self' 'unsafe-inline' 'unsafe-eval' ${clerkDomain} https://js.stripe.com https://www.googletagmanager.com https://www.google-analytics.com`,
`style-src 'self' 'unsafe-inline' ${clerkDomain}`,
`connect-src 'self' ${clerkDomain} ${convexDomain} https://api.stripe.com https://www.google-analytics.com https://analytics.google.com`,
// ... rest of CSP
].join('; ');
例:カスタム CDN の追加
const cdnDomain = process.env.NEXT_PUBLIC_CDN_URL
? new URL(process.env.NEXT_PUBLIC_CDN_URL).origin
: '';
const csp = [
"default-src 'self'",
`script-src 'self' ${cdnDomain}`,
`style-src 'self' ${cdnDomain}`,
`img-src 'self' ${cdnDomain} https:`,
// ... rest of CSP
].join('; ');
よくある CSP の問題と解決策
問題 1:インラインスクリプトがブロックされる
症状: <script> タグ内のスクリプトが実行されない
解決策: Next.js では unsafe-inline と unsafe-eval が必要です。これらはすでに CSP で設定されています。
より適切な代替策: nonce を使用する(SSR の変更が必要です)。
const nonce = crypto.randomBytes(16).toString('base64');
script-src 'self' 'nonce-${nonce}'
問題 2:外部画像が読み込まれない
症状: Gravatar などのユーザーアバターが表示されない
現在の解決策: img-src 'self' data: https: blob: はすべての HTTPS 画像を許可します
より厳格な代替策:
`img-src 'self' data: https://gravatar.com https://images.yourapp.com`
問題 3:WebSocket 接続に失敗する
症状: Convex のリアルタイム更新が機能しない
解決策: connect-src に Convex ドメインが含まれていることを確認します。
`connect-src 'self' ${convexDomain}`
セキュリティヘッダーが防ぐ攻撃
✅ クリックジャッキング(X-Frame-Options) ✅ XSS の増幅(CSP) ✅ MIME 混同(X-Content-Type-Options) ✅ SSL ストリッピング(HSTS) ✅ 非公開データが検索エンジンに露出すること(X-Robots-Tag) ✅ データの流出(CSP connect-src) ✅ 許可されていないリソースの読み込み(CSP default-src)
よくある避けるべき間違い
❌ CSP にドメインをハードコードしないでください - 環境変数を使用してください
❌ 開発環境では HSTS を有効にしないでください - localhost で動作しなくなります
❌ X-Frame-Options: ALLOW-FROM を使用しないでください - 非推奨です
❌ デプロイ後にヘッダーをテストし忘れないでください
❌ 過度に寛容な CSP(* ワイルドカードなど)を設定しないでください
✅ 環境変数を使った動的な CSP を使用してください ✅ デプロイ後にオンラインツールでヘッダーをテストしてください ✅ 新しいサードパーティサービスを追加したら CSP を更新してください ✅ HSTS は本番環境でのみ有効にしてください ✅ ダッシュボードのルートがインデックスされないように保護してください
参考資料
- Mozilla セキュリティヘッダーガイド:https://infosec.mozilla.org/guidelines/web_security
- OWASP セキュアヘッダープロジェクト:https://owasp.org/www-project-secure-headers/
- コンテンツセキュリティポリシー:https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP
- HSTS 仕様:https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security
- セキュリティヘッダーチェッカー:https://securityheaders.com/
次のステップ
- XSS を防止するには:
input-validationスキルを使用してください - クリックジャッキングをテストするには:
security-testingスキルを使用してください - ヘッダーは自動的に設定されます - 実装については
middleware.ts:1を確認してください - 新しいインテグレーションを追加するには:middleware.ts の CSP を更新してください