【実務・中級編】 APIにおけるCSRF(クロスサイトリクエストフォージェリ)対策とカスタムヘッダー – Web APIアーキテクチャ・データ連携実践ガイド

REST APIの「ステートレス」という甘い罠:CSRF対策とカスタムヘッダーの正しい流儀

ネットワークエンジニアとして現場を渡り歩いていると、若手から「REST APIはステートレスなんだから、セッション管理も不要だしCSRFなんて関係ないですよね?」という質問をよく受ける。

結論から言えば、それは半分正解で、半分は命取りになる危険な誤解だ。Web APIがステートレスであっても、ブラウザが勝手にクッキーを送信する仕様である以上、CSRF(クロスサイトリクエストフォージェリ)のリスクは常に口を開けて待っている。今日は、RFCの仕様とブラウザの挙動という「ネットワークの深淵」から、泥臭い実務での防御策を紐解いていこう。

—

1. なぜステートレスなAPIにCSRFリスクが宿るのか

RESTの原則における「ステートレス性」とは、サーバー側がクライアントのコンテキスト(状態)を保持しないことを意味する。しかし、多くのモダンなWebアプリでは、認証に Cookie を使用しているはずだ。

ここで思い出してほしい。ブラウザは、対象のドメインに対するリクエストであれば、たとえそれが悪意ある別サイトからのクロスドメインリクエストであっても、自動的に Cookie を付与して送信するという仕様を持っている。

もし、APIが Cookie ベースの認証を採用しているなら、ユーザーがログインした状態で悪意のあるページを開くだけで、APIは「本人が意図したリクエストだ」と勘違いして実行してしまう。これがCSRFの悪夢だ。

—

2. カスタムヘッダーが「最強の盾」になる理由

CSRFを防ぐ最もシンプルかつ強力な手法が、X-Requested-With のようなカスタムヘッダーの強制だ。

なぜこれが効くのか? それは、ブラウザの CORS(Cross-Origin Resource Sharing) の制約にある。JavaScript(Fetch API や XMLHttpRequest)でカスタムヘッダーを付与したリクエストを投げると、ブラウザは「プリフライトリクエスト(OPTIONSメソッド)」を先に投げて、サーバーに「このカスタムヘッダーを許可するか?」を事前に確認する。

悪意のある攻撃者が <img> タグや <form> 要素を使ってリクエストを投げても、カスタムヘッダーを付与することはできない。つまり、サーバー側で「カスタムヘッダーがないリクエストは門前払い」というルールを設けるだけで、CSRFの攻撃経路を物理的に遮断できるわけだ。

—

3. 実践:サーバーサイドでの防御実装例

まずは、Pythonの FastAPI を例に、ヘッダーの存在をチェックするミドルウェアの実装を見てみよう。

from fastapi import FastAPI, Request, HTTPException, status

app = FastAPI()

@app.middleware("http")
async def verify_custom_header(request: Request, call_next):
    # APIの重要なエンドポイントのみチェックをかけるのがポイント
    if request.method in ["POST", "PUT", "DELETE"]:
        # X-Requested-With ヘッダーの有無を確認
        if request.headers.get("X-Requested-With") != "XMLHttpRequest":
            raise HTTPException(
                status_code=status.HTTP_403_FORBIDDEN,
                detail="CSRF防御:不正なリクエスト元です"
            )
    
    return await call_next(request)

この実装により、curlや不正なスクリプトからのリクエストは、ヘッダーがない限り 403 Forbidden で弾かれることになる。

—

4. クライアントサイドでの実装(Fetch API)

フロントエンド側では、APIリクエストを投げる際に必ずこのヘッダーを付与する共通関数を用意するのが定石だ。

// Fetch APIを使用したリクエストのラッパー
async function secureApiCall(url, options = {}) {
    const headers = {
        'Content-Type': 'application/json',
        'X-Requested-With': 'XMLHttpRequest', // CSRF対策用のカスタムヘッダー
        ...options.headers
    };

    const response = await fetch(url, { ...options, headers });
    return response.json();
}

—

5. 現場のトラブルシューティング:デバッグの視点

インフラエンジニアとして現場でよくある失敗は、CORS設定の不備だ。カスタムヘッダーを導入すると、ブラウザは必ず OPTIONS リクエストを送る。サーバーがこれに適切に応答しないと、本番のリクエスト自体がブロックされてしまう。

もし「なぜかAPIが叩けない」という事態に陥ったら、まずは以下の curl コマンドでプリフライトリクエストの挙動を確認してほしい。

# プリフライトリクエストのテスト
curl -v -X OPTIONS http://api.example.com/data \
  -H "Origin: http://your-app.com" \
  -H "Access-Control-Request-Method: POST" \
  -H "Access-Control-Request-Headers: X-Requested-With"

このレスポンスヘッダーに Access-Control-Allow-Headers: X-Requested-With が含まれているか。ここが設定の分かれ道だ。

—

まとめ:ネットワークの流儀を忘れずに

カスタムヘッダーによる防御は、SameSite 属性のクッキー設定と並び、現代のWeb API設計における「守りの要」だ。

1. APIにはカスタムヘッダーを強制する(ブラウザのCORS制約を逆手に取る)。
2. 必ずOPTIONSリクエストを許可する(CORS設定を適正に行う)。
3. Cookieの SameSite=Lax または Strict を併用する(多層防御の鉄則)。

プロトコルの仕様を深く理解し、ブラウザという「クライアントの挙動」を味方につける。これこそが、堅牢なAPIを構築するエンジニアの矜持だ。ぜひ、皆さんの設計にもこの「一枚のヘッダー」を取り入れてみてほしい。

コメント

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