【実務・中級編】CSRF対策としてのカスタムHTTPヘッダー検証とCORSの併用戦略 – アプリケーションセキュリティ & 安全な開発防御ガイド

なぜ「CSRF対策=トークン埋め込み」だけでは足りないのか?

こんにちは。セキュリティの現場に身を置いていると、「CSRF対策はセッションにトークンを仕込めば完璧」という神話を信じ切っているエンジニアによく出会います。確かにそれは教科書的な正解ですが、現代のモダンなWebアーキテクチャにおいては、それだけでは「穴」が空くことがあります。

特に、SPA(Single Page Application)とAPIサーバーが分離された構成では、Cookieベースの認証を維持しつつ、柔軟なクロスオリジン通信を許可しなければなりません。ここで誤ったCORS設定を行うと、トークンの存在すら無力化される脆弱性が生まれます。今日は、カスタムHTTPヘッダーとCORSポリシーを組み合わせた、防御の多重化について深掘りしましょう。

1. 攻撃者が狙う「ブラウザの盲点」

CSRF(クロスサイト・リクエスト・フォージェリ)の本質は、ユーザーのブラウザに保存された「認証情報(Cookie)」を、攻撃者のサイトから勝手に利用させることです。

ここで攻撃者がよく使うのが、「単純リクエスト(Simple Requests)」の仕組みです。ブラウザは特定の条件を満たすGETやPOSTリクエスト(Content-Type: application/x-www-form-urlencodedなど)であれば、CORSのプリフライト(OPTIONSメソッド)を飛ばさずに実行します。もしあなたのAPIがこれを受け入れてしまうと、攻撃者が仕掛けた罠サイトから、バックグラウンドで勝手にPOSTリクエストが飛んでいくことになります。

2. 多重防御の切り札:カスタムヘッダー検証

この攻撃を防ぐ最強の武器の一つが、「カスタムHTTPヘッダーの強制」です。

ブラウザの仕様上、JavaScriptでカスタムヘッダー(例: X-Requested-With や X-CSRF-Token)を付与しようとすると、必ずCORSのプリフライトリクエストが発生します。サーバー側で「このヘッダーがないリクエストは一切受け付けない」というルールを徹底すれば、単純リクエストによるCSRFは物理的に不可能になります。

—

3. 実装サンプル:防御の鉄則

以下は、Python (FastAPI) とフロントエンド (JavaScript) を想定した実装例です。

【サーバー側】FastAPIでの検証ロジック

単にヘッダーをチェックするだけでなく、CORS設定で許可するオリジンを厳密に制限することが鉄則です。

from fastapi import FastAPI, Request, HTTPException, Header

app = FastAPI()

厳密なCORS設定:信頼できるドメインのみ許可
origins = [“https://your-app.com”]

@app.middleware(“http”)
async def verify_custom_header(request: Request, call_next):
# プリフライトリクエストはパスする
if request.method == “OPTIONS”:
return await call_next(request)

# ここでカスタムヘッダーの存在を確認
# 攻撃者はブラウザの制約上、クロスオリジンでこのヘッダーを付与できない
if request.headers.get(“X-Requested-With”) != “XMLHttpRequest”:
raise HTTPException(status_code=403, detail=”CSRF防御:不正なリクエストです”)

return await call_next(request)

【フロントエンド】JavaScriptによるリクエスト

フロントエンド側では、すべてのAPIコール時に必ずこのヘッダーを付与するように共通化します。

// Axios等のインターセプターで全リクエストに付与
axios.interceptors.request.use(config => {
config.headers[‘X-Requested-With’] = ‘XMLHttpRequest’;
return config;
});

—

4. インフラレベルでの防御(Nginxの設定)

アプリケーション層だけでなく、WAFやリバースプロキシでもガードを固めておきましょう。もし開発者がうっかりコード側でチェックを忘れても、Nginxで弾くことが可能です。

server {
# 不正なヘッダーを持つリクエストを遮断する例
if ($http_x_requested_with != “XMLHttpRequest”) {
# 特定のパスを除外したい場合はここで調整
return 403;
}

# CORS設定:許可されたオリジン以外からのアクセスを拒否
add_header ‘Access-Control-Allow-Origin’ ‘https://your-app.com’ always;
add_header ‘Access-Control-Allow-Methods’ ‘GET, POST, OPTIONS’ always;
add_header ‘Access-Control-Allow-Headers’ ‘X-Requested-With, Content-Type’ always;
}

5. 現場の教訓:なぜこれで防げるのか?

この対策の美しさは、「ブラウザのセキュリティモデルを逆手に取っている」点にあります。

1. プリフライトの強制: カスタムヘッダーを付けることで、ブラウザは必ず「このオリジンからのリクエストを許可するか?」とサーバーに伺いを立てます(OPTIONSメソッド)。
2. ホワイトリストの適用: サーバーはCORS設定で「許可されたドメイン」以外からのOPTIONSリクエストを拒否します。
3. 偽装の不可能性: 攻撃者が罠サイトからリクエストを飛ばしても、カスタムヘッダーを付ける権限(オリジン間の制約)がないため、プリフライトでブロックされるか、そもそもヘッダーを付与できないため、アプリ側の検証で門前払いされます。

まとめ:セキュリティは「掛け算」で考える

「トークンがあれば大丈夫」という思考停止は、インシデントの入り口です。

  • Cookieの SameSite=Strict/Lax 設定(基本のキ)
  • カスタムヘッダーによるプリフライトの誘発
  • 厳格なCORSポリシー

これらを重ねることで、仮に一つの脆弱性が突破されても、システム全体が即座に崩壊することはありません。セキュリティとは「いかに攻撃者にコストを払わせるか」のゲームです。今日紹介した多重防御を実装し、攻撃者が「このシステムを攻めるのは割に合わない」と諦めるレベルまで引き上げましょう。

何か不明点があれば、またいつでも聞いてください。現場のコードで悩むことがあれば、それが一番の勉強ですから。

コメント

タイトルとURLをコピーしました