【実務・中級編】 OAuth 2.0のRedirect URI検証不備によるトークン流出 – オフェンシブセキュリティ & リバースエンジニアリング防御ガイド

OAuth 2.0の「Redirect URI」はただの文字列ではない:トークン強奪を阻止する設計の要諦

現場でOAuthの実装を見ていると、開発者が最も軽視しがちなのが redirect_uri のバリデーションだ。「正規表現でなんとなく弾いているから大丈夫だろう」という甘い認識が、どれほど簡単にアカウント乗っ取りへと繋がるか、今日はその恐ろしさと、二度と穴を開けないための鉄壁の防御策を共有しよう。

なぜ「リダイレクト先の検証」で失敗するのか?

OAuthのフローにおいて、認可サーバーはアクセストークン(またはコード)を redirect_uri に送り込む。ここで攻撃者が狙うのは、バリデーションロジックの「緩さ」だ。

よくある失敗パターンを挙げよう。

1. 前方一致・部分一致の罠: https://app.example.com/callback を許可するつもりが、https://app.example.com.attacker.com を許してしまう実装。
2. ワイルドカードの乱用: https://*.example.com/* のように許可してしまうと、攻撃者が所有するサブドメイン(例: https://dev-test.example.com.attacker.com)にトークンが漏洩する。
3. クエリパラメータの不正利用: redirect_uri=https://app.example.com/callback?continue=https://evil.com のように、リダイレクト先のさらに先を指定させる「オープンリダイレクト」との合わせ技。

攻撃者は、被害者がログインした瞬間に発行される access_token を自分のサーバーのログに記録し、それを使って被害者のリソースを好き勝手に操作する。これが「アカウント乗っ取り」のシナリオだ。

—

実践:攻撃の手口(PoCの思考プロセス)

攻撃者は、認可サーバーが許可している redirect_uri のリストを特定した後、そのドメイン配下の「Open Redirect」脆弱性を探す。

もし、https://app.example.com/redirect?url=... のようなエンドポイントがあれば、攻撃者は以下のようなURLを作成する。

https://auth.example.com/authorize?client_id=123&redirect_uri=https://app.example.com/redirect?url=https://attacker.com/log&response_type=token

認可サーバーは redirect_uri が許可リスト内にあると判断してリダイレクトを実行するが、最終的には攻撃者のサーバーにトークン付きで転送されることになる。

—

「コピペで防ぐ」セキュアな実装ガイド

防御の鉄則は「ホワイトリストによる完全一致比較」だ。正規表現やパターンの使用は極力避け、許可されたURIの集合を配列やDBに持ち、厳密に比較する。

1. PHPによるセキュアなバリデーション例

文字列比較は === を使用し、曖昧さを排除する。

/**
 * 許可されたリダイレクトURIのリスト
 */
$allowed_redirect_uris = [
    'https://app.example.com/callback',
    'https://app.example.com/oauth-success'
];

/**
 * リクエストされたURIがホワイトリストに存在するか厳密にチェック
 */
function is_valid_redirect_uri($request_uri, $allowed_list) {
    // 厳密な比較を行うため、URLエンコードされた状態の差異を考慮する必要がある場合は
    // parse_urlで構造を分解してから比較するのがベスト
    return in_array($request_uri, $allowed_list, true);
}

$request_uri = $_GET['redirect_uri'] ?? '';

if (!is_valid_redirect_uri($request_uri, $allowed_redirect_uris)) {
    // ログに記録し、攻撃の兆候としてアラートを上げる
    error_log("Invalid Redirect URI attempt: " . $request_uri);
    die("Error: Invalid redirect_uri.");
}

2. Python (FastAPI) での実装

モダンなフレームワークでも、型ヒントと厳密な比較を徹底する。

from fastapi import HTTPException, Query
from typing import List

ALLOWED_URIS: List[str] = [
    "https://app.example.com/callback"
]

def validate_redirect_uri(redirect_uri: str = Query(...)):
    # リストに存在しない場合は即座に遮断
    if redirect_uri not in ALLOWED_URIS:
        raise HTTPException(status_code=400, detail="Invalid redirect_uri")
    return redirect_uri

3. Nginx での防御(エッジでのガード)

アプリケーション層に届く前に不正なパラメータを弾く設定だ。

# 特定のパス以外で不正な redirect_uri パラメータが含まれていたらブロックする例
if ($arg_redirect_uri !~* "^https://app\.example\.com/callback$") {
    return 403;
}

※注:nginxでの判定は非常に複雑になりやすいため、基本はアプリケーション層での検証を推奨する。これはあくまで多層防御の一環だ。

—

セキュリティチーフからの「現場の心得」

1. 動的なRedirect URI生成は悪: 「ユーザーごとにURLを変えたい」という要件があっても、state パラメータを活用して、アプリケーション内部でリダイレクト先を管理すること。決してURLパラメータでリダイレクト先を決定してはならない。
2. state パラメータは必須: トークン流出だけでなく、CSRF攻撃を防ぐためにも state パラメータを生成し、バリデーションを行うこと。
3. ログを疑え: もしシステムログに redirect_uri への不審なアクセスが記録されたら、それは単なるバグではなく、攻撃者があなたの門を叩いている音だ。

セキュリティは「魔法のコード」を探すことではなく、「どうすればこの穴を塞げるか」を執拗に考え続けることに他ならない。今回紹介したホワイトリスト方式は地味だが、最強の防御だ。さあ、今すぐ君のコードを確認してくれ。もし * や regex が見えたら、今日がその修正のチャンスだ。

コメント

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