redirect_uri 検証の仕様比較
redirect_uri(リダイレクト URI )の検証方法は、OAuth 2.0 の歴史とともに厳格化されてきました。このドキュメントでは、各仕様における検証ルールの違いを比較し、セキュリティと利便性のバランスを考察します。
第1部: 概要
なぜ redirect_uri の検証が重要なのか?
redirect_uri は認可コードやトークンの送信先です。検証が甘いと、攻撃者が認可コードを自分のサーバーに送らせることができます。
正常なフロー:
ユーザー → 認可サーバー → https://legitimate-app.com/callback?code=xxx
攻撃(オープンリダイレクタ):
ユーザー → 認可サ ーバー → https://attacker.com/steal?code=xxx
↑ 認可コードが攻撃者に渡る
検証の厳格さスペクトラム
緩い 厳格
│ │
▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ 登録なし │ │ 部分一致 │ │ 完全一致 │ │ 完全一致 │ │ 完全一致 │
│ (非推奨) │ │ (RFC6749) │ │ (OIDC) │ │ + 制約 │ │ + 制約 │
│ │ │ │ │ │ │ (BCP) │ │ (FAPI) │
└──────────┘ └──────────┘ └──────────┘ └──────────┘ └──────────┘
利便性: 高 ◄─────────────────────────────────────────────────► 低
安全性: 低 ◄─────────────────────────────────────────────────► 高
RFC 3986: URI 比較の基礎
redirect_uri の検証を理解するには、まず URI の仕様である RFC 3986 を理解する必要があります。
URI の構造
https://user:pass@example.com:8080/path/to/resource?query=value#fragment
└─┬─┘ └───┬───┘ └────┬────┘└─┬┘└──────┬────────┘└─────┬─────┘└───┬───┘
scheme userinfo host port path query fragment
└────── ────┬──────────┘
authority
| 要素 | 説明 | 大文字小文字 | RFC 3986 セクション |
|---|---|---|---|
| scheme | プロトコル(http, https) | 区別なし | 3.1 |
| userinfo | ユーザー情報(非推奨) | 区別あり | 3.2.1 |
| host | ホスト名または IP | 区別なし | 3.2.2 |
| port | ポート番号 | - | 3.2.3 |
| path | リソースパス | 区別あり | 3.3 |
| query | クエリパラメータ | 区別あり | 3.4 |
| fragment | フラグメント識別子 | 区別あり | 3.5 |
URI の比較方法(RFC 3986 Section 6)
RFC 3986 では URI を比較する 4 つの方法を定義しています。
1. Simple String Comparison(単純文字列比較)
Two URIs are equivalent if they are identical character-by-character.
最も厳格な方法。文字単位で完全に一致するかを確認。
比較対象: https://example.com/callback
✅ 一致: https://example.com/callback
❌ 不一致: https://EXAMPLE.COM/callback ← 大文字
❌ 不一致: https://example.com:443/callback ← デフォルトポート明示
❌ 不一致: https://example.com/callback/ ← 末尾スラッシュ
❌ 不一致: https://example.com/Callback ← パスの大文字小文字
OAuth 2.0 Security BCP(RFC 9700)はこの方法を要求。
2. Syntax-Based Normalization(構文ベース正規化)
正規化してから比較。以下の変換を行う:
| 正規化 | 例 |
|---|---|
| スキームを小文字化 | HTTPS: → https: |
| ホストを小文字化 | Example.COM → example.com |
| パーセントエンコードを大文字化 | %2f → %2F |
| 不要なパーセントエンコードを解除 | %41 → A |
空パスを / に | https://example.com → https://example.com/ |
| デフォルトポートを削除 | :443 → (削除) |
正規化前: HTTPS://Example.COM:443/Path
正規化後: https://example.com/Path
正規化前: https://example.com
正規化後: https://example.com/
この方法を使うと、以下が同一視される:
https://example.com/callback
https://EXAMPLE.COM/callback ← ホストは正規化で同一
https://example.com:443/callback ← デフォルトポートは正規化で削除
3. Scheme-Based Normalization(スキームベース正規化)
スキーム固有のルールを適用。例えば HTTP では:
- 空パスを
/に変換 - デフォルトポート(80/443)を削除
4. Protocol-Based Normalization(プロトコルベース正規化)
実際にリソースにアクセ スして等価性を判断。リダイレクト先が同じなら同一視するなど。
セキュリティ上の理由から、OAuth では使用しない。
なぜ Simple String Comparison が推奨されるか?
┌─────────────────────────────────────────────────────────────────────────┐
│ 正規化を行うと... │
│ │
│ 登録: https://app.example.com/callback │
│ │
│ リクエスト: HTTPS://APP.EXAMPLE.COM/callback │
│ ↓ 正規化 │
│ https://app.example.com/callback │
│ ↓ │
│ ✅ 一致(正規化後) │
│ │
│ これは一見便利だが... │
│ │
│ 攻撃者が細工した URI: │
│ https://app.example.com%2F..%2F..%2Fattacker.com/callback │
│ ↓ 正規化(パーセントデコード) │
│ https://app.example.com/../../attacker.com/callback │
│ ↓ パス正規化 │
│ https://attacker.com/callback ← 攻撃者のサーバーに! │
│ │
└─────────────────────────────────────────────────────────────────────────┘
正規化は予期しない変換を引き起こす可能性があり、セキュリティリスクになる。
各仕様での URI 比較方法
| 仕様 | 比較方法 | 根拠 |
|---|---|---|
| RFC 6749 | 明記なし | 実装依 存 |
| OIDC Core | 明記なし(「identical」とだけ記載) | 解釈の余地あり |
| RFC 8252 | Simple String Comparison を推奨 | Section 8.4 |
| RFC 9700 | Simple String Comparison を必須 | Section 4.1.3 |
| FAPI 1.0/2.0 | exact string matching | プロファイルで規定 |
実装上の注意点
パーセントエンコードの扱い
同じ文字列でもエンコード方法が異なると不一致になる:
登録: https://example.com/callback?state=a b
リクエスト: https://example.com/callback?state=a%20b
Simple String Comparison では ❌ 不一致
対策: 登録時にパーセントエンコード済みの形式で保存する。
末尾スラッシュ
登録: https://example.com/callback
リクエスト: https://example.com/callback/
Simple String Comparison では ❌ 不一致
対策: 登録時に末尾スラッシュの有無を統一するポリシーを決める。
IPv6 アドレス
登録: http://[::1]/callback
リクエスト: http://[0:0:0:0:0:0:0:1]/callback
Simple String Comparison では ❌ 不一致(同じアドレスでも)
対策: IPv6 は正規化形式([::1])で登録することを推奨。
第2部: 仕様別の検証ルール
RFC 3986(URI 仕様, 2005年)
redirect_uri 検証の基礎となる URI 構造と比較方法を定義。OAuth 関連仕様はすべてこの RFC を参照している。
URI の構造
https://user:pass@www.example.com:8080/path/to/resource?query=value#fragment
└─┬─┘ └───┬──┘ └──────┬───────┘└─┬┘└─── ────┬───────┘└─────┬────┘└───┬───┘
scheme userinfo host port path query fragment
└──────────────────┬──────────────────┘
authority
| 構成要素 | 説明 | 大文字小文字 | RFC 3986 | redirect_uri での扱い |
|---|---|---|---|---|
| scheme | プロトコル | 区別しない | 3.1 | 必須(本番は https) |
| userinfo | ユーザー情報 | 区別する | 3.2.1 | 通常使用しない |
| host | ホスト名 | 区別しない | 3.2.2 | 必須 |
| port | ポート番号 | - | 3.2.3 | 省略時はデフォルト |
| path | リソースパス | 区別する | 3.3 | 完全一致が求められる |
| query | クエリパラメータ | 区別する | 3.4 | 仕様により扱いが異なる |
| fragment | フラグメント | 区別する | 3.5 | 禁止(OAuth/OIDC 共通) |
URI 比較の 3 つの方式(Section 6)
6.2.1 Simple String Comparison(単純文字列比較)
バイト単位での完全一致。最も厳格で安全。
比較対象: https://example.com/callback
✅ 一致: https://example.com/callback
❌ 不一致: https://EXAMPLE.COM/callback ← 大文字
❌ 不一致: https://example.com:443/callback ← ポート明示
❌ 不一致: https://example.com/callback/ ← 末尾スラッシュ
❌ 不一致: https://example.com/Callback ← パスの大文字
RFC 9700(Security BCP)はこの方式を要求。
6.2.2 Syntax-Based Normalization(構文ベース正規化)
URI 構文ルールに基づいて正規化してから比較。
| 正規化ルール | 変換例 |
|---|---|
| スキーム小文字化 | HTTPS: → https: |
| ホスト小文字化 | EXAMPLE.COM → example.com |
| パーセントエンコード大文字化 | %2f → %2F |
| 不要なエンコード解除 | %41 → A(非予約文字) |
| 空パス正規化 | https://example.com → https://example.com/ |
| デフォルトポート削除 | :443 → (削除) |
6.2.3 Scheme-Based Normalization(スキームベース正規化)
スキーム固有のルールを適用。HTTP では末尾スラッシュや /../ の解決など。
3 方式の比較
| URI ペア | Simple String | Syntax-Based | Scheme-Based |
|---|---|---|---|
http://example.com vs HTTP://example.com | ❌ | ✅ | ✅ |
http://example.com vs http://example.com/ | ❌ | ❌ | ✅ |
http://example.com:80 vs http://example.com | ❌ | ✅ | ✅ |
http://example.com/~a vs http://example.com/%7Ea | ❌ | ✅ | ✅ |