OAuth 認可サーバー実装のセキュリティ
OAuth authorize endpoint は GET 表示と POST 承認を分離する
3か月前
OAuth の /authorize では、GET リクエストで authorization code を即発行せず、ログイン状態と request validation を確認したうえで同意画面を返す。code 発行はユーザーが approve した POST のみで行い、consent 用の短寿命 token を cookie と hidden field の両方で照合すると、意図しない承認 POST や CSRF 的な操作を防ぎやすい。
OAuth code と refresh token の消費は条件付き更新で再利用レースを防ぐ
3か月前
authorization code や refresh token を一度だけ使わせる場合、読み取り後に別 update するだけでは並行リクエストで再利用される可能性がある。used_at IS NULL や revoked_at IS NULL、expires_at > now の条件を含む update を transaction 内で実行し、affected rows が 1 件のときだけ token pair を作成すると、再利用レースを防ぎやすい。
公開エンドポイントのレート制限キーは、接続元が信頼プロキシ範囲内のときだけ信頼ヘッダーを採用する
3か月前
未認証で受け付ける公開エンドポイント(動的クライアント登録、ログイン等)にはレート制限が必須になる。このときキーに何を使うかで、偽装耐性と可用性のどちらかを落としやすい。
二つの失敗モード
- クライアント申告ヘッダーをそのまま信じる: framework のクライアント IP 取得関数は、信頼プロキシを明示設定しないと既定で全プロキシを信頼し、転送ヘッダーの最左=クライアントが自由に書ける値を返す実装がある。この場合、値を毎回変えるだけで制限を丸ごと回避でき、被害者の IP を名乗れば標的型のロックアウトもできる。任意文字列がキーになるためエントリ膨張も起こせる。
- 接続元アドレス(TCP ピア)をそのまま使う: 前段にプロキシや CDN がある構成では、これは常にプロキシのアドレスになる。結果、全ユーザーが1つのバケットを共有し、誰か1人の試行で全員が締め出される可用性の穴になる。偽装はできないが DoS として使える。
「偽装されないから接続元アドレスを使う」は前者を避けて後者に落ちるだけで、解決になっていない。
判断基準
- 信頼ヘッダー(前段が実クライアント値で上書きするヘッダー)は、接続元アドレスが信頼プロキシの範囲内にあるときだけ採用する。範囲外なら無視して接続元アドレスへ落とす。判定に使う接続元アドレスは転送ヘッダーの影響を受けない値であること。
- framework が「このヘッダーを信じる」系の設定を持っていても、接続元を検証せず無条件にヘッダーを返す実装なら使わない。信頼リストの判定より前に返す実装が実際にある。
- 信頼プロキシ設定は空でも明示的に適用する。既定が全プロキシ信頼の実装では、設定しないこと自体が「全部信じる」になる。
- 前段の構成に依存する値(ヘッダー名・プロキシの範囲)はコードに埋め込まず設定に出す。インフラを移したら設定を差し替えるだけにする。既定値は「何も信頼しない」にして、未設定環境で勝手に緩まないようにする。
実測してから決める
どのヘッダーが実際に届くか、どれが前段に上書きされるかは推測で決めない。外すと DoS 穴か偽装バイパスのどちらかになり、どちらの失敗も外からは見えない。
一時的な診断ログを入れて本番へ1リクエスト送り、接続元アドレス・転送ヘッダーの全要素・候補ヘッダーの実値を観測する。偽装した値を送ったときに前段が上書きするか素通しするかも同じ手順で確認する。得られるのは設定値であってコード構造ではないので、将来インフラを移しても同じ手順で測り直せる。
落とし穴
- ローカル開発では接続元アドレスが実クライアントになるため、この不具合は本番でだけ再現する。テストが通ることは、その環境で設定が効いていることを意味しない。
- 転送ヘッダーの「実クライアントは右から N 番目」という位置決め打ちは、前段のホップ数構成が変わると壊れる。
- 前段プロキシの公開 IP レンジを信頼リストに入れる方式は、レンジ更新の保守が永続的に残り、古いと黙って劣化する。
検証方法
- 信頼プロキシ範囲外からの偽装ヘッダーが無視され、接続元アドレスがキーになること(攻撃シナリオそのものの回帰テスト)。
- 信頼プロキシ範囲内なら信頼ヘッダーの値がキーになること。
- 設定が空なら転送ヘッダーを一切採用しないこと。
- 本番では、転送ヘッダーを毎回変えながら上限を超える回数を送り、拒否されること。偽装が有効なら要求ごとにバケットが分かれて拒否されない。
OAuth ログイン中の初回ユーザー作成では return URL を setup 完了まで保持する
3か月前
OAuth authorize 中に外部ログインを開始し、まだアプリ側ユーザー作成や初期設定が完了していない場合、ログイン callback だけで return URL を消費すると OAuth フローへ戻れなくなる。authorize への return URL は短寿命 cookie などで setup 完了まで保持し、完了後に安全性を検証してから /authorize に戻すと、初回ユーザーでも連携フローを継続できる。
OAuth consent 画面の CSP は埋め込みブラウザの form 送信制約も検証する
3か月前
OAuth consent 画面を外部クライアントの埋め込みブラウザや connector UI で表示する場合、通常のブラウザで通る CSP でも form-action が送信をブロックすることがある。form-action の allowlist を入れても同一の送信先が拒否される環境では、consent route に限って form-action directive を外し、script-src 'none'、object-src 'none'、base-uri 'none'、frame-ancestors 'none'、hidden token と cookie 照合など他の防御を残す選択肢がある。
OAuth consent の redirect URL は保存元が自前でも検証してから使う
3か月前
OAuth authorize への return URL を cookie などに保存してログイン後に redirect する場合、保存処理が自前 middleware だけでも、使用直前に host、scheme、path を検証する。許可する path を /oauth/authorize などに限定し、host は空または現在の host、scheme は空または http/https のように絞ると、cookie 改ざんや将来のコード変更による open redirect を防ぎやすい。