mcpField
S
security-headers

security-headers

github.com/harperaaセキュリティ19202 回インストール45 週間前に更新
リポジトリ

security-headers について

security-headersは、ミドルウェアでWebアプリにブラウザー向けセキュリティヘッダーを設定する方法を解説する開発者向けスキルです。CSP、クリックジャッキングやMIME対策、本番環境のHSTS、非公開ルートのインデックス制御を扱います。

主な機能

  1. 01Content-Security-Policyの設定
  2. 02X-Frame-Optionsによるクリックジャッキング対策
  3. 03nosniffによるMIME対策
  4. 04本番環境のみのHSTS
  5. 05ダッシュボードルートのインデックス抑制

活用シーン

  1. 01ミドルウェアでWebリクエストにヘッダーを付与
  2. 02CSPで必要なサードパーティドメインを許可
  3. 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 により自動的に適用されます。手動で設定する必要はありません。すでに保護されています。

適用するヘッダー
  1. Content-Security-Policy (CSP) - リソースの読み込みを制御します
  2. X-Frame-Options: DENY - クリックジャッキングを防ぎます
  3. X-Content-Type-Options: nosniff - MIME タイプの混同を防ぎます
  4. Strict-Transport-Security (HSTS) - HTTPS を強制します(本番環境のみ)
  5. 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 ヘッダーに厳密に従い、推測しないようブラウザーに指示します。

重要な理由: このヘッダーがないと、攻撃者は次のことができます。

  1. <script>evil()</script> を含む「image.jpg」をアップロードする
  2. <img src="/uploads/image.jpg"> でリンクする
  3. ブラウザーがファイルをスニッフィングしてスクリプトを検出し、実行する
  4. アップロード時の検証をすり抜けて 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 を一度設定すると、ブラウザーはその設定を記憶します。解除する必要がある場合は、次の手順が必要です。

  1. max-age=0 を設定する
  2. すべてのユーザーのブラウザーが新しいヘッダーを受信するまで待つ
  3. 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
オンラインでのヘッダーテスト

デプロイ済みのサイトをテストするには、次のオンラインツールを使用してください。

新しい連携に合わせた 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 は本番環境でのみ有効にしてください ✅ ダッシュボードのルートがインデックスされないように保護してください

参考資料

次のステップ

  • XSS を防止するには:input-validation スキルを使用してください
  • クリックジャッキングをテストするには:security-testing スキルを使用してください
  • ヘッダーは自動的に設定されます - 実装については middleware.ts:1 を確認してください
  • 新しいインテグレーションを追加するには:middleware.ts の CSP を更新してください

コメント