認可コードの横取り攻撃対策でのPKCE
認可コード横取り攻撃とは、パブリッククライアント(モバイルアプリやSPAなど)で悪意のあるアプリが認可コードを盗み出し、アクセストークンを不正に取得する攻撃手法です。 今回は認可コード横取り攻撃とPKCE(RFC7636でOAuthの拡張仕様として定義されている)を使うことで対策されていることを確認し、個人的に思うPKCEのききどころについてまとめます。
背景・想定読者
背景はOAuthの仕様は必ずしも安全というわけではなく、場合によってPKCEのような仕様も必要になることの説明にチャレンジします。 想定読者は、認可コードフローについてなんとなく知っているという方向けです。
従来のフローと認可コード横取り
今回の認可コード横取り攻撃の説明をしてみます。 認可コード横取り攻撃はその名の通りなんですが、攻撃者が発行された認可コードを使ってアクセストークンを取りに行けてしまいます。
私は普段WebのエンジニアをしているのでSPAを例に説明してみようと思います。 想定するシナリオは以下です。
- 攻撃用のブラウザ拡張機能がすでに入っている(攻撃アクターがわかりやすいように)
- 拡張機能が入っているブラウザでSPAでログインボタンを押下
- ASに認可リクエストが送られ、認可コードが帰ってくる
- ブラウザ上で認可コードを奪取されてしまう。
シーケンスにしてみると以下のようになります。
sequenceDiagram
autonumber
participant SPA as SPA (Vue/Reactなど)
participant Browser as ブラウザ (本体)
participant Atc as 攻撃者のサーバー
participant Extension as 悪意のある拡張機能
participant AS as 認可サーバー (AS)
SPA ->> Browser: ログインボタン押下
Browser ->> AS: 認可リクエスト送信
AS -->> Browser: Redirect (Location: SPAのURL?code=XYZ123)
rect rgb(110, 130, 110)
activate Extension
Browser ->> Extension: URLや通信から認可コードを取得
Extension ->> Atc: 取得した認可コードを送信
deactivate Extension
Atc ->> AS: 認可コードを使ってトークンエンドポイントへリクエスト
AS -->> Atc: トークン取得
end
Browser ->> SPA: スクリプト起動
SPA ->> Browser: リダイレクト
Browser ->> AS: 認可コードを使ってトークンエンドポイントに問い合わせ
AS ->> AS : 同じ認可コードの2回目以降のリクエストはdeny
認可コードを受け取った人が入れ替わったことを検知しないことが原因で横取りされてしまいました。 横取りされているところは色付きで示しています。 具体的にはSPAから認可コードフローを開始したにもかかわらず、先に攻撃者が認可コードを取得・トークン交換まで済ましてしまったのです。
一番最後、同じ認可コードの2回目以降のトークンエンドポイントへのリクエストは拒絶されるのは必須ですが、1回目のトークンの無効化は必須ではないため攻撃者のアクセストークンが無効になるかどうか実装次第になってしまいますね。。 (必須要件を満たしているかなんてことを疑い出したくないです。)
PKCEを眺めてみる
PKCEとはどこが拡張されて、認可コードの横取りを対策できるのでしょう? PKCEでは以下のパラメータが追加されます。詳しくはRFC7636を読みましょう。。
- code_verifier
- ランダムな文字列が入っているパラメータ
- code_challenge_method
code_challengeの計算方法を指定するパラメータ
- code_challenge
code_verifierをcode_challenge_methodで計算した値
個人的な印象ですが、だいたいSHA256でハッシュしてBase64URLエンコードされた値がcode_challengeに入っている気がします。 (詳細な仕様については追えていません、、)
では実際にこのパラメータで防げるのか、PKCEでのトークンの横取りをみていきましょう
sequenceDiagram
autonumber
participant SPA as SPA (Vue/Reactなど)
participant Browser as ブラウザ (本体)
participant Atc as 攻撃者のサーバー
participant Extension as 悪意のある拡張機能
participant AS as 認可サーバー (AS)
SPA ->> Browser: ログインボタン押下
Note over SPA,AS: code_verifierを作成<br>それをハッシュ化した code_challenge も用意
Browser ->> AS: 認可リクエスト送信 (w/ code_challenge)
AS -->> Browser: Redirect (Location: SPAのURL?code=XYZ123)
rect rgb(110, 130, 110)
activate Extension
Browser ->> Extension: URLや通信から認可コードを取得
Extension ->> Atc: 取得した認可コードを送信
deactivate Extension
Atc ->> AS: トークン交換リクエスト
AS -->> Atc: Bad Request
end
Browser ->> SPA: スクリプト起動
SPA ->> Browser: リダイレクト
Browser ->> AS: トークン交換リクエスト(w/ code_verifier)
AS ->> AS: 最初に受け取った code_challenge と code_verifier が一致するか検証
AS -->> Browser: トークン取得
実際にトークンの取得をリクエストしている部分を比べてみましょう。
攻撃者はトークン交換に認可コードを取得してASに問い合わせていますが、code_verifierが不足しています。
ASは認可コードを送ってきた相手をcode_verifierとcode_challengeを照合することで確認しますが、code_verifierの値が足りません(もしくはデタラメな値で照合に失敗するでしょう)。
一方、ユーザーはcode_verifierをトークン交換時に一緒に送信することで認可リクエストした相手と同一人物であることを照合し、トークンを返します。
認可コードだけ取得できればトークン取得できたフローでは無くなったわけです!めでたし、めでたし?
しかし、PKCEでも気をつけなければならないことはcode_verifierというランダム文字列や、それをhash化したcode_challengeから推測できないような値にする必要があります。
クライアント実装も気をつけないといけませんね(今始まったことではないんだが)
まとめ
大雑把にトークンの横取りというシナリオから、PKCEというOAuthの拡張仕様で防ぐことができそうという説明をしてみました。 ここ間違っていそうなどあれば、Twitter/Bluesky等でDMなどしていただけますと幸いです!