OAuth 2.0の「入口」を塞げ:リダイレクトURIの検証不備が招く致命的な帰結
現場のセキュリティ運用を見ていると、OAuth 2.0の実装で「動くこと」を優先しすぎて、「守ること」を疎かにしているケースが非常に多い。特に、認可サーバーがリダイレクトURIを甘く判定した結果、アクセストークンを攻撃者のサーバーへ横流しされる事故は後を絶たない。
今日は、なぜ「正規表現による緩い検証」が地獄への入り口なのか、そしてどう実装すれば完封できるのか、泥臭い実務の視点から解説する。
—
1. なぜ「オープンリダイレクタ」は悪夢なのか
OAuth 2.0において、リダイレクトURIの検証が不十分だと、攻撃者は正規の認可サーバーを経由して、被害者のアクセストークンを自らの管理するサーバーへと転送させることができる。
攻撃シナリオ (PoCの流れ)
1. 攻撃者の準備: 攻撃者は自身のサーバーに、クエリパラメータをログ出力する簡単なスクリプトを設置する。
2. 巧妙な誘導: 攻撃者は、正規の認可サーバーに対して、redirect_uri を「攻撃者のサーバー」に変更した認可リクエストを作成し、被害者に踏ませる。
3. 認可コードの奪取: 被害者が承認ボタンを押すと、正規サーバーは「認可コード」を攻撃者のサーバーへ送信する。
4. 成りすまし: 攻撃者はそのコードを使い、正規のトークンエンドポイントでアクセストークンを入手。被害者のアカウントにログイン完了。
この手口の厄介なところは、「正規の認可サーバーからリクエストが来ている」ため、ブラウザやWAFが異常を検知しにくい点にある。
—
2. 禁じ手:やってはいけない実装
「サブドメインなら許容しよう」「パスさえ合っていればいいだろう」という安易なコードは、今すぐ削除してほしい。
// 危険な実装例:これだけは絶対にやるな
$allowed_domain = “example.com”;
if (strpos($_GET[‘redirect_uri’], $allowed_domain) !== false) {
// 攻撃者が “example.com.attacker.jp” を用意すれば簡単に突破される
}
—
3. 完封するための「ホワイトリスト」実装
対策の基本は完全一致比較 (Exact Match) だ。ホワイトリストに登録されたURIと、リクエストされたURIを、文字列として1バイトの狂いなく比較する。これ以外の選択肢はない。
Python (Flask) でのセキュアな実装例
from urllib.parse import urlparse
事前に登録された正当なリダイレクトURIのリスト
ALLOWED_REDIRECT_URIS = [
“https://app.example.com/callback”,
“https://app.example.com/oauth/callback”
]
def validate_redirect_uri(redirect_uri):
# 1. リクエストされたURIが存在するか
if not redirect_uri:
return False
# 2. ホワイトリストとの完全一致を確認
# URLエンコードの差異を避けるため、正規化された状態で比較するのがベスト
if redirect_uri in ALLOWED_REDIRECT_URIS:
return True
return False
使い方
requested_uri = request.args.get(‘redirect_uri’)
if not validate_redirect_uri(requested_uri):
# エラーを返し、処理を中断する(重要!)
abort(400, “Invalid redirect URI”)
—
4. インフラ層での防御(多重防衛)
アプリケーションコードの修正が即座にできない場合や、念のための多重防衛として、ゲートウェイ側で制御する方法も有効だ。
Nginx でのリクエスト制限
Nginxの設定で、許可されていないクエリパラメータを弾くことも可能だが、OAuthのリダイレクトURIは動的であるため、基本的には「認可サーバー側」での厳密なバリデーションが正攻法である。
もしリダイレクト先を固定できる環境であれば、以下のように strict-transport-security と併せて信頼できるドメインのみを許可する設計を徹底してほしい。
—
5. 現場のセキュリティチーフからの提言
最後に、実務で絶対に守ってほしいルールを3つだけ伝える。
1. ワイルドカードは悪: https://.example.com のような設定は、攻撃者にサブドメインの乗っ取りやクロスサイトスクリプティング(XSS)の踏み台を提供することと同義だ。
2. エラーハンドリング: 検証失敗時は、ユーザーに詳細なヒントを与えず、シンプルに「無効なリクエスト」として処理を中断すること。詳細なエラーメッセージは攻撃者にヒントを与える。
3. ライフサイクル管理: 開発環境や検証環境のURIが、誤って本番環境のホワイトリストに入り込んでいないか、デプロイパイプラインで自動チェックする仕組みを作ること。
セキュリティとは、完璧な製品を買うことではなく、こうした「当たり前のことを、当たり前に、厳格に」積み重ねる規律のことだ。コードを書く際、常に「もし自分が攻撃者なら、この入力をどう悪用するか?」という視点を忘れないでほしい。
君たちの書くコードが、次のインシデントを防ぐ最後の砦になる。健闘を祈る。
コメント