OAuth 2.0の「リダイレクトURI」という名のパンドラの箱:トークン窃取を防ぐための防壁設計
現場でコードをレビューしていると、いまだに「リダイレクトURIの検証」を単なる形式的なチェックだと勘違いしているエンジニアに出くわす。OAuth 2.0のフローにおいて、認可コード(Authorization Code)やアクセストークンを攻撃者に渡してしまうミスは、まさに「玄関の鍵をかけずに、泥棒に手紙を渡して帰宅してもらう」ようなものだ。
今日は、OAuth 2.0の認可フローで最も狙われやすい「リダイレクトURIの不備」と、それを確実に仕留めるための防御策について、現場の知見を詰め込んで解説する。
—
1. なぜ「リダイレクトURIの不備」が致命的なのか?
OAuth 2.0の認可フローでは、ユーザーが認可ボタンを押すと、認可サーバーはあらかじめ登録された redirect_uri へとユーザーをリダイレクトさせる。この際、認可コードやトークンがURLのクエリパラメータとして付与される。
ここで、もし redirect_uri を動的に受け入れ、検証が甘い(例:ドメインのプレフィックスチェックのみ、あるいは正規表現がガバガバ)場合、攻撃者は以下のようなPoCを仕掛ける。
攻撃シナリオ(トークン窃取)
1. 攻撃者が用意した悪意あるサイト attacker.com を作成する。
2. 認可サーバーへのリクエストを操作し、redirect_uri=https://victim-app.com.attacker.com/callback といった細工を施す。
3. ユーザーが認可をクリックすると、認可コードが攻撃者のサーバーに送信される。
4. 攻撃者はそのコードを使い、正規の認可サーバーからアクセストークンを取得する。
これが、「オープンリダイレクタ」や「不十分な検証」が引き起こす最悪のシナリオだ。
—
2. 厳格な検証が、唯一の正解
「検証」の定義を改めよう。「後方一致」や「文字列の先頭が含まれているか」といった甘い判定は、セキュリティの世界では「無いもの」と同義だ。
やるべきことは一つ。ホワイトリストによる「完全一致(Exact Match)」検証だ。
Python (Flask) での実装例
多くのフレームワークで使えるロジックだ。設定ファイルに許可されたURIをリスト化し、リクエストが来た際に比較する。
from flask import request, abort
from urllib.parse import urlparse
厳格なホワイトリスト:認可サーバーに登録したものと完全に一致させる
ALLOWED_REDIRECT_URIS = [
“https://app.example.com/callback”,
“https://app.example.com/oauth/callback”
]
def validate_redirect_uri(provided_uri):
# 1. URLパースを行い、不正な形式を弾く
if not provided_uri:
return False
# 2. 完全一致での検証(これが最強の防御)
if provided_uri not in ALLOWED_REDIRECT_URIS:
# 本来ならログに記録し、管理者にアラートを飛ばすべき
print(f”警告: 不正なリダイレクト試行: {provided_uri}”)
return False
return True
使用例
@app.route(‘/authorize’)
def authorize():
redirect_uri = request.args.get(‘redirect_uri’)
if not validate_redirect_uri(redirect_uri):
abort(400, “Invalid Redirect URI”)
# 認可処理へ進む…
—
3. インフラ層での防御:WAFによる二重の防壁
アプリケーション層での実装はもちろん必須だが、コードの修正漏れやライブラリの脆弱性を考慮し、WAF(AWS WAFやCloudFront Functionsなど)で防御を重ねるのがプロの流儀だ。
Nginx (リバースプロキシ) での例
Nginxで動的にバリデーションを行うのは骨が折れるが、特定のパターン以外を弾く設定は有効だ。
特定の認可エンドポイント以外への不正なパラメータを弾く設定例
location /oauth/authorize {
# redirect_uriパラメータが含まれる場合、ホワイトリスト以外のホストを弾く(簡易版)
if ($arg_redirect_uri !~ “^https://app\.example\.com/callback”) {
return 403;
}
proxy_pass http://backend_app;
}
※ただし、インフラ層のルールは保守が面倒になりがちだ。基本は認可サーバー側で厳格に管理するのが、モダンなアーキテクチャの鉄則である。
—
4. チーフエンジニアからの教訓
最後に、現場で生き残るための「思考の癖」を伝えておく。
- 「動的」は罪である: リダイレクト先を動的に生成する設計自体が、この脆弱性の温床だ。どうしても動的にしたい場合は、URIのハッシュ値やトークンによる間接参照を使うなど、設計段階で複雑性を排除すること。
- ログを見ろ: 認証周りのエラーログは、攻撃の予兆を教えてくれる最高の手がかりだ。
400 Bad Requestを返して終わりではなく、どのような不正リクエストが来ているかをSentryやDatadogで監視し、異常なスパイクがないか常に目を光らせてほしい。 - Stateパラメータを忘れるな: 今回のテーマではないが、OAuth 2.0では
stateパラメータによるCSRF対策も必須だ。これらが組み合わさって初めて、堅牢な認証基盤と言える。
セキュリティは「ここまでやれば完璧」というゴールはない。しかし、今日紹介したような「完全一致のホワイトリスト」といった地味で泥臭い実装の積み重ねが、何年経っても揺るがない堅牢なシステムを形作るんだ。
さあ、今すぐコードベースを確認して、contains() や正規表現で妥協している箇所がないか、チェックを始めてくれ。健闘を祈る。
コメント