認可コード横取りの「死角」を突く:PKCEがなぜ「必須」なのか、現場の視点で語ろう
こんにちは。インシデント対応の最前線にいると、「なぜこんな初歩的な設計ミスが?」と溜息をつきたくなる瞬間が多々あります。特にOAuth 2.0のフロー、君たちは正しく理解して実装しているだろうか?
今日は、パブリッククライアント(SPAやモバイルアプリなど、クライアントシークレットを隠し持てない環境)における最大の弱点、「認可コード横取り攻撃(Authorization Code Interception Attack)」と、それを無効化する PKCE (Proof Key for Code Exchange) について、現場の泥臭い話を交えて解説する。
—
1. なぜ「認可コード」が盗まれるのか?
OAuth 2.0の標準フローでは、ユーザーが認可サーバーでログインした後、ブラウザ(またはOS)を介してクライアントアプリへ「認可コード」が渡される。
もし君のアプリがモバイルアプリなら、OSの「カスタムURLスキーム」を悪用されると、攻撃者が仕掛けた別のアプリがその認可コードを横取りできる。ブラウザベースのSPAなら、ブラウザ拡張機能やXSSがそのコードを盗み取る。
認可コードさえ手に入れれば、攻撃者は自分のサーバーでトークンリクエストを発行し、ユーザーになりすましてAPIを叩くことができてしまう。「シークレットがない」というパブリッククライアントの宿命が、この脆弱性の正体だ。
—
2. PKCEの仕組み:攻撃者に「鍵」を無効化させる
PKCE(ピーシーイー)は、「認可コードを要求した本人」しか、そのコードを使えないようにするための仕組みだ。
1. クライアント: 乱数から code_verifier を生成し、そのハッシュ値 code_challenge を認可リクエストに含める。
2. 認可サーバー: その code_challenge を保管しておく。
3. 攻撃者: 認可コードを盗む(が、code_verifier を持っていない)。
4. クライアント: トークン取得時、本物の code_verifier を送る。
5. 認可サーバー: 送られてきた値をハッシュ化し、最初に保存した code_challenge と照合。一致すれば許可。
攻撃者は code_verifier を知らないため、盗んだコードをトークンに交換することができない。シンプルだが、極めて強力な防御策だ。
—
3. 実践:PKCEのセキュアな実装(JavaScript/Web Crypto API)
現場では、ライブラリを使うのが鉄則だが、中身がどう動いているかを知っておく必要がある。以下は、ブラウザネイティブのWeb Crypto APIを使った code_verifier と code_challenge の生成コードだ。
// 1. ランダムな文字列 (code_verifier) を生成
function generateVerifier() {
const array = new Uint8Array(32);
window.crypto.getRandomValues(array);
// Base64URLエンコードして返却
return btoa(String.fromCharCode(…array))
.replace(/\+/g, ‘-‘).replace(/\//g, ‘_’).replace(/=/g, ”);
}
// 2. code_challenge を生成 (SHA-256ハッシュ化)
async function generateChallenge(verifier) {
const encoder = new TextEncoder();
const data = encoder.encode(verifier);
const digest = await window.crypto.subtle.digest(‘SHA-256’, data);
// Uint8ArrayをBase64URLエンコード
return btoa(String.fromCharCode(…new Uint8Array(digest)))
.replace(/\+/g, ‘-‘).replace(/\//g, ‘_’).replace(/=/g, ”);
}
// 使用例
const verifier = generateVerifier();
const challenge = await generateChallenge(verifier);
// この ‘challenge’ を認可リクエストの code_challenge パラメータに設定し、
// ‘verifier’ はトークン交換時までブラウザのセッションストレージ等に一時保存する。
—
4. セキュリティチーフからの「現場の掟」
PKCEを実装する際、多くのエンジニアが陥る罠がいくつかある。これだけは守ってくれ。
- S256を強制せよ: PKCEには「plain」というハッシュ化しないモードもあるが、これはデバッグ用だ。本番環境では必ず
code_challenge_method=S256を使用すること。plainは脆弱性が残るため、攻撃者に突破されるリスクがある。 - トークンエンドポイントでの検証: 認可サーバー側の実装では、
code_challengeの保存とcode_verifierの照合が適切に行われているか、Auth0やKeycloakなどのIdP側で設定が「Strict」になっているかを確認すること。 - stateパラメータとの併用: PKCEは認可コード横取り対策だが、CSRF対策としては依然として
stateパラメータが有効だ。PKCEがあるからといってstateを省略してはいけない。
WAFでの対策補足
もし認可サーバーの入り口にWAF(AWS WAF等)を置いているなら、認可リクエストに含まれる code_challenge が不正な文字を含んでいないか(インジェクションの兆候がないか)をバリデーションするルールを組むことも検討してほしい。
—
最後に:セキュリティは「完璧」を求めず「継続」を求めろ
「PKCEを実装したからもう安心だ」――そう思った瞬間が一番危ない。OAuth 2.0の仕様はアップデートされ続けている。今回紹介したPKCEは、今のWeb開発における「最低限の礼儀」だ。
もし君が開発しているアプリで、まだPKCEに対応していないフローが残っているなら、明日、一番の優先順位でバックログに入れてほしい。セキュリティは、コードの行数ではなく、こうした「リスクの芽を摘む執念」から生まれるものだ。
さて、次は「JWTのトークン検証における秘密鍵の管理」について話をしようか。準備ができたらまた聞いてくれ。
コメント