なぜ今さら「PKCEなし」のOAuth 2.0が許されないのか:認可コード横取りの現実
現場でコードをレビューしていると、いまだに「PKCE(Proof Key for Code Exchange)? うちのアプリは公開クライアントじゃないから不要でしょ」という甘い言葉を耳にする。ハッキリ言おう。それは、鍵のかかっていない玄関に「不在です」と張り紙をしているようなものだ。
OAuth 2.0の認可コードフローにおいて、PKCEを導入しないことは、現代のWebアプリケーションにおいて「致命的な設計上の欠陥」と言っても過言ではない。今回は、なぜPKCEが不可欠なのか、そして現場で即座に実装すべき防御策を解説する。
—
1. 認可コード横取り攻撃のメカニズム(PoCの視点)
PKCEがない場合、認可コードはブラウザの「リダイレクト」という非常に脆弱な経路を通る。攻撃者は、以下の手法で認可コードを盗み出す。
1. カスタムURLスキームの悪用: モバイルアプリや一部のデスクトップアプリで、独自のスキーム(例: myapp://callback)を登録している場合、攻撃者は同じスキームを悪意あるアプリで登録し、OSが発行する認可コードを横取りする。
2. ブラウザ履歴やRefererの漏洩: 認可コードがURLパラメータとして露骨に露出しているため、ブラウザの履歴や、外部サイトへのリンクをクリックした際の Referer ヘッダから第三者に盗聴されるリスクがある。
攻撃者のPoC(概念):
攻撃者は、正当なアプリになりすまして Authorization Server から認可コードを受け取り、それを Token Endpoint に送り込むことで、アクセストークンを不正に取得する。この時、サーバー側は「クライアントIDが一致している」という表面的な情報しか見ていないため、正規のユーザーになりすませてしまうのだ。
—
2. PKCEがもたらす「証明」の力
PKCEは、このフローに「検証」という名のスパイスを加える。クライアントは認可リクエストを送る前に、ランダムな文字列(code_verifier)を生成し、そのハッシュ値(code_challenge)をサーバーに送信する。
トークン交換時に、本物の code_verifier を提示しない限り、サーバーはアクセストークンを発行しない。これにより、途中で認可コードを盗み見られても、攻撃者は code_verifier を持っていないため、トークンを生成できなくなる。
—
3. 実装サンプル:JavaScript (Frontend) でのPKCEフロー
フロントエンドで code_verifier を生成し、ハッシュ化してリクエストを送るまでの定石だ。
// 1. ランダムな文字列(Verifier)を生成
const generateVerifier = () => {
const array = new Uint8Array(32);
window.crypto.getRandomValues(array);
return btoa(String.fromCharCode(...array))
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
};
// 2. SHA-256でハッシュ化し、Base64URLエンコードしてChallengeを作成
async function generateChallenge(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(/=/g, '');
}
// 使用例
const verifier = generateVerifier();
const challenge = await generateChallenge(verifier);
// この 'challenge' を認可リクエストの 'code_challenge' パラメータに設定する
// 後でサーバーに送るために 'verifier' はセッションストレージ等に一時保存しておく
sessionStorage.setItem('code_verifier', verifier);
—
4. サーバーサイドでの検証(Python/Flask等の例)
サーバー側では、初回リクエスト時に code_challenge を保存し、トークンエンドポイントで受け取った code_verifier をハッシュ化して照合する。
import hashlib
import base64
def verify_pkce(verifier, expected_challenge):
# 受け取った verifier を SHA-256 でハッシュ化
hashed = hashlib.sha256(verifier.encode('utf-8')).digest()
# Base64URLエンコード
actual_challenge = base64.urlsafe_b64encode(hashed).decode('utf-8').replace('=', '')
# 照合
return actual_challenge == expected_challenge
# トークンエンドポイントでの処理例
# if verify_pkce(request_form['code_verifier'], stored_challenge):
# issue_access_token()
# else:
# raise Exception("PKCE検証に失敗しました")
—
5. 運用上の鉄則:設計への組み込み
インフラや設計レベルでこの脆弱性を完全に封じ込めるには、以下の設定も併せて行う必要がある。
- OAuthクライアント設定: 認可サーバー(Auth0, Keycloak, AWS Cognito等)の設定画面で、該当クライアントの「PKCEを必須にする(Require PKCE)」フラグを必ずONにすること。
- Redirect URIの厳密化:
localhostやワイルドカードを利用したRedirect URIは絶対に避ける。完全一致(Exact Match)のみを許可するポリシーを徹底せよ。 - WAFでの異常検知: 短時間に同一の
code_challengeを使用したリクエストが大量に発生している場合、レートリミットをかけて遮断するようにNginxやAWS WAFをチューニングしておくこと。
最後に:エンジニアとしての矜持
「動けばいい」という考えは、開発者ではなく「コードを書く作業者」の思考だ。セキュリティは「コスト」ではなく「プロダクトの信頼という資産」そのものだ。
今回紹介したPKCEの実装は、わずか数十行のコードだが、これだけで認可コード横取りという古典的かつ破壊的な攻撃を無効化できる。ぜひ、今すぐ自社の認証フローを見直し、PKCEが有効になっているか確認してほしい。もし無効なら、それは今日、君の手で修正すべき「負債」だ。
コメント