どうして認可コードフローのトークンエンドポイントは redirect_uri を要求するのか

OIDC で redirect_uri の登録と完全一致が必須だという前提で物事を考えていたため、どういう攻撃が成立するのかわからず混乱した。 微妙に確信が持てないので間違ってたらおしえてください。

認可コードフローおさらい

  1. 認可リクエストを送る
    • redirect_uri
    • client_id
    • その他
  2. コールバックで認可コードが redirect_uri に送られてくる
  3. コールバックを受け取ったクライアントはトークンエンドポイントに認可コードを送って、アクセストークンを得る
    • クライアントの認証がある
    • このときに認可リクエストで送信した redirect_uri も送る これなんで必要なのかの話
      • 完全一致してる必要がある

RFC での言及

10.6. 認可コードリダイレクトURIの操作より引用。

認可要求に認可コードグラントタイプを用いるとき, クライアントは redirect_uri パラメーターによりリダイレクトURIを指定できる. 攻撃者がリダイレクトURIの値を操作可能であるとき, 認可サーバーによってリソースオーナーのユーザーエージェントを攻撃者の管理下にあるURIに認可コードを含んだ状態でリダイレクトさせることができる.

攻撃者は, 正しいクライアントにおいてアカウントを作成し, 認可フローを開始することができる. 攻撃者のユーザーエージェントがアクセス許可のために認可サーバーに送られたとき, 攻撃者は正当なクライアントにより提供された認可URIを取得し, クライアントのリダイレクトURIを攻撃者の管理下にあるURIに交換する. 攻撃者はその後, 正当なクライアントに向けた操作された認可アクセスリンクをたどるよう被害者を騙す.

認可サーバーにおいて, 被害者は正当で信頼できるクライアントによる正常で有効なリクエストを促され, そのリクエストを認可する. 被害者はその後, 認可コードとともに攻撃者の管理下にあるエンドポイントにリダイレクトされる. 攻撃者は, クライアントにより提供されたオリジナルのリダイレクトURIを用いて, 認可コードを送ることにより認可フローを完了する. クライアントは認可コードとアクセストークンを交換し, それを攻撃者のクライアントのアカウントと紐づけることで, 被害者により (クライアント経由で) 認可された保護されたリソースへのアクセス権を獲得できる.

このような攻撃を防ぐため, 認可サーバーは認可コードの取得に用いたリダイレクトURIと, 認可コードとアクセストークンの交換時に提供されたリダイレクトURIが同一であることを確認しなければならない (MUST). 認可サーバーはパブリッククライアントに対してリダイレクトURIの登録を要求しなければならず (MUST), コンフィデンシャルクライアントに対してもリダイレクトURIの登録を要求すべきである (SHOULD). リダイレクトURIがリクエストにより提供されたとき, 認可サーバーは登録された値を用いてそれを検証しければならない (MUST).

なんとなく理解した内容

認可リクエストの redirect_uri に攻撃者が管理しているサイトの URI を指定されたときに、認可コードが攻撃者のサイトに飛ぶ。 その認可コードを正規のクライアントにコールバックとして送信すると他人のアクセストークンを保持したクライアントを操作できる。

詳細

認可リクエストに以下のようなパラメーターを指定して、被害者に踏ませる。

client_id=攻撃対象のクライアント&redirect_uri=https://攻撃者のドメイン.example.com

認可コードが https://攻撃者のドメイン.example.com に送られるので、攻撃者は被害者が認可を行った認可コードを手に入れる。

攻撃者はこの認可コードをクライアントの本来の redirect_uri に送信する。 このときトークンエンドポイントが redirect_uri の検証を行っていない場合、クライアントが認可コードとトークンの引き換えに成功する。 認可を行ったのは攻撃者ではなく被害者なので、トークンは被害者に紐付いている。

この状態で攻撃者がリソースサーバーに対して自らのプロフィールを取得する API を利用するような操作を行うと、プロフィールは被害者のものとなり、攻撃者は被害者の個人情報を取得することが出来る。

redirect_uri の検証を行った場合、認可レスポンスを受け取った攻撃対象のクライアントは、トークンエンドポイントに https://攻撃者のドメイン.example.com ではなく https://攻撃対象のドメイン.example.com を送信する。(そもそも攻撃対象のクライアントは攻撃者のredirect_uri を知らない)

こうなると認可リクエストで送信した redirect_uriトークンエンドポイントに送った redirect_uri が別の値になるので、攻撃が失敗する。 ようするに認可リクエストを行ったときに指定した redirect_uri にちゃんとコールバックで認可コードが渡って、その redirect_uri のクライアント自身がトークンリクエストをしているよね?というところが担保できる。

シーケンス

攻撃者が被害者の認可コードを取得

f:id:inabajunmr:20201009233637p:plain

認可リクエストの client_id は正規のクライアントとなる。

攻撃者が被害者に紐付いたアクセストークンを利用

f:id:inabajunmr:20201009233646p:plain

認可コードをアクセストークンに引き換えには

  • クライアント認証
  • 認可コードを発行したクライアントからのトークンリクエス

が必要だが、認可リクエストの client_id が正規のクライアントのものなのでどちらの条件も満たしている。

redirect_uri の検証を行う場合

f:id:inabajunmr:20201009234043p:plain

トークンエンドポイントで redirect_uri の検証をしている場合、認可コードを送信した redirect_uriトークンリクエストを送ってきたクライアントが想定している redirect_uri が異なるので、攻撃を防ぐことが出来る。

RFC6749 ではコンフィデンシャルクライアントの redirect_uri の事前登録を MUSTとしていない

3.1.2.2. 登録要件より引用。

認可サーバーは, 以下のようなクライアントに対してリダイレクトエンドポイントの事前登録を要求すること (MUST): パブリッククライアント. インプリシットグラントタイプを利用するコンフィデンシャルクライアント. 認可サーバーは, すべてのクライアントに対して, 認可エンドポイントアクセス前にリダイレクトURIの事前登録を要求すべきである (SHOULD).

この攻撃は認可コードグラントのみを使うコンフィデンシャルクライアントに対しても有効だが、その場合の redirect_uri の事前登録は MUST ではない。 また、マッチングの条件については以下の記述がある。

認可サーバーは, クライアントに (一部分ではなく) 完全なリダイレクトURIの登録を要求するべきである (SHOULD). (クライアントはリクエスト毎のカスタマイズするために state リクエストパラメーターを使用できる (MAY)) 完全なリダイレクトURIの登録を要求することができない場合, 認可サーバーはURIスキーム, オーソリティー, パス (認可要求の時, リダイレクトURIのクエリーコンポーネントのみ動的に変更を許可する) の登録を要求するべきである (SHOULD).

この SHOULD を守っていない場合、パスに指定した値によってオープンリダイレクタが可能になっていると、redirect_uri を事前登録していてもこの攻撃は成立する気がする。

参考

シーケンス書いてから見直したらまるっきり同じ話だった。

OAuth 2.0 の code は漏れても大丈夫ってホント!? - OAuth.jp