OAuth 2.0の「穴」を塞ぐ:認可コード横取り攻撃とPKCEの絶対防衛論
現場でインシデント対応をしていると、いまだに「認可コード(Authorization Code)が漏れても、クライアントシークレットがあれば安全だ」という勘違いをしているエンジニアに出くわします。しかし、現実は甘くない。特にモバイルアプリやSPA(シングルページアプリケーション)のような「シークレットを隠しきれない」環境において、攻撃者はリダイレクトURIの隙間を縫って、あなたのユーザーのトークンをかっさらう準備をしています。
今日は、OAuth 2.0/OIDCの認可フローにおける「認可コード横取り攻撃」のメカニズムを紐解き、PKCE(Proof Key for Code Exchange)という防弾チョッキをどう実装すべきか、現場の視点から解説します。
—
1. なぜ「認可コード」が奪われるのか?(攻撃の構図)
かつてのOAuth 2.0フローでは、攻撃者は以下のような手口で認可コードを奪取していました。
1. カスタムURLスキームの悪用: モバイルアプリ等で利用される myapp://callback のようなスキームを、攻撃者が自身の悪意あるアプリで登録しておく。
2. リダイレクトの横取り: 認可サーバーから発行された認可コードを含んだリダイレクト先を、OSのインテント(Intent)やブラウザの脆弱性を突いて攻撃者のアプリが横取りする。
3. トークン交換: 認可コードさえ手に入れれば、クライアントIDを偽装(あるいは知っている状態)して認可サーバーへ投げ、アクセストークンを不正取得する。
ここで最も重要なのは、「認可コードそのものが漏洩した際、それを正当な持ち主以外が使えないようにする」という仕組みが、初期のOAuthには欠けていた点です。
—
2. PKCE:認可フローを「鍵と扉」でロックする
PKCE(RFC 7636)の真骨頂は、「認可コードを取得する際のリクエストと、トークンを交換する際のリクエストが、同一のクライアントであること」を数学的に証明することにあります。
PKCEのステップ
1. Code Verifier(乱数)生成: クライアントが一時的な秘密鍵(code_verifier)を作成する。
2. Code Challenge算出: code_verifier をSHA-256でハッシュ化し、Base64URLエンコードしたもの(code_challenge)を認可サーバーに送る。
3. トークン交換時に検証: 最後にトークンを要求する際、オリジナルの code_verifier を送信。サーバー側で「受け取った code_verifier をハッシュ化したら、最初に送られてきた code_challenge と一致するか?」を確認する。
攻撃者は code_verifier を知らないため、いくら認可コードを盗んでも、トークンへの交換という「最後の扉」を開くことはできません。
—
3. 実践:PKCE対応のセキュアな実装(JavaScript/Web)
ReactやVueなどのSPAでPKCEを実装する場合、以下のようなロジックが必要です。
// 1. ランダムな文字列(Code Verifier)を生成
function generateRandomString(length) {
const array = new Uint8Array(length);
window.crypto.getRandomValues(array);
return btoa(String.fromCharCode(...array))
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
// 2. SHA-256でハッシュ化してCode Challengeを作成
async function generateCodeChallenge(verifier) {
const encoder = new TextEncoder();
const data = encoder.encode(verifier);
const digest = await window.crypto.subtle.digest('SHA-256', data);
return btoa(String.fromCharCode(...new Uint8Array(digest)))
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
// 使用例
const codeVerifier = generateRandomString(64);
const codeChallenge = await generateCodeChallenge(codeVerifier);
// この後、認可サーバーへの認可リクエストに code_challenge と code_challenge_method=S256 を付与する
—
4. インフラレベルでの防御:リダイレクトURIの厳格化
コードがどれだけセキュアでも、設定がガバガバでは意味がありません。認可サーバー(Auth0, Okta, Keycloak等)側の設定で、以下のルールを徹底してください。
- 完全一致(Exact Match)の原則:
https://app.example.com/*のようにワイルドカードを使うのはNGです。攻撃者がhttps://app.example.com.attacker.com/のようなサブドメインを悪用してリダイレクトを誘導する余地を与えます。必ずフルパスで登録してください。 - HTTPSの強制: HTTPによるリダイレクトは、中間者攻撃(MITM)に対して無防備です。WAFやロードバランサーで強制的にHTTPSへリダイレクトさせ、Refererヘッダーによる情報漏洩を防ぐため
Referrer-Policy: no-referrerを付与するのが定石です。
Nginxでのヘッダー設定例
# セキュリティヘッダーの追加
add_header Referrer-Policy "no-referrer" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Content-Security-Policy "default-src 'self'; connect-src 'self' https://auth.example.com;" always;
—
最後に:エンジニアへのメッセージ
PKCEの実装は、もはや「推奨」ではなく「必須」です。OAuth 2.0の仕様(RFC 6749)は柔軟すぎるがゆえに、実装者のミスがそのまま脆弱性に直結します。「とりあえず動くコード」を書くことは簡単ですが、その背後でユーザーのセッションがどう守られているかを理解することこそが、プロフェッショナルの仕事です。
まずは今すぐ、あなたのアプリケーションの認可リクエストを確認してください。code_challenge を送っていますか? 認可サーバーの設定でリダイレクトURIが適切に制限されていますか?
「性悪説」に基づいた実装こそが、あなたのシステムを堅牢にする唯一の近道です。 攻撃者は常にその「仕様の隙間」を狙っています。その隙間を、PKCEという鉄壁で埋めてやりましょう。
コメント