【実務・中級編】OAuth 2.0 における認可コード横取り攻撃とPKCEの必須化 – アプリケーションセキュリティ & 安全な開発防御ガイド

OAuth 2.0の「認可コード横取り」はなぜ防げないのか?PKCE強制化がエンジニアの必須教養である理由

現場でコードをレビューしていると、いまだに「OAuth 2.0の認可フローなら安全だよね」という甘い認識に遭遇することがある。断言しよう。PKCE(Proof Key for Code Exchange)なしの認可コードフローは、現代のWeb環境では「鍵のかかっていない玄関」に等しい。

今日は、なぜ攻撃者が認可コードを狙うのか、そして私たちが実装で何をすべきか、インシデントの最前線から解説する。

—

1. なぜ「認可コード」が狙われるのか?

OAuth 2.0の認可コードフローでは、ブラウザを介して code がリダイレクトされる。このとき、攻撃者は以下の手段で code を横取りしようと試みる。

  • カスタムURLスキームのハイジャック: モバイルアプリやデスクトップアプリが特定のスキーム(例: myapp://)をリッスンしている場合、攻撃者が同じスキームを登録して横取りする。
  • ブラウザの履歴やログ: 認可コードがブラウザの履歴やプロキシサーバーのログに残ることで、攻撃者がそれを回収する。

本来、サーバーサイドのクライアント(機密情報を持つバックエンド)であれば client_secret で身元を証明できるが、SPA(シングルページアプリケーション)やモバイルアプリといった「公開クライアント」には秘密を隠し持てない。ここで登場するのが PKCE だ。

—

2. PKCEの本質:動的な「ワンタイム鍵」

PKCEは、認可リクエスト時に「自分だけが知っている秘密(Code Verifier)」をハッシュ化し、その「ハッシュ値(Code Challenge)」をあらかじめ送る。認可コードを受け取った後、改めて「元の秘密」を提示させることで、横取りした攻撃者がコードを使えないようにする仕組みだ。

攻撃シナリオ(PoCイメージ)

1. 正当なアプリ: code_challenge を送って認可を要求。
2. 攻撃者: 中間者攻撃でリダイレクトから code を盗む。
3. 攻撃者: 盗んだ code でトークンを要求するが、code_verifier を持っていないため、認可サーバーに拒否される。

—

3. 実装のベストプラクティス(Node.js/JavaScript)

現代のWeb開発では、oidc-client-ts や msal などのライブラリを使うのが定石だが、仕組みを理解するために「PKCEの核」となる部分を実装しよう。

// PKCEのCode VerifierとChallengeを生成する関数
async function generatePKCE() {
// 1. ランダムな文字列 (Code Verifier) を生成
const array = new Uint8Array(32);
window.crypto.getRandomValues(array);
const verifier = btoa(String.fromCharCode.apply(null, array))
.replace(/\+/g, ‘-‘).replace(/\//g, ‘_’).replace(/=+$/, ”);

// 2. SHA-256でハッシュ化し、Code Challengeを作成
const encoder = new TextEncoder();
const data = encoder.encode(verifier);
const digest = await window.crypto.subtle.digest(‘SHA-256’, data);

const challenge = btoa(String.fromCharCode.apply(null, new Uint8Array(digest)))
.replace(/\+/g, ‘-‘).replace(/\//g, ‘_’).replace(/=+$/, ”);

return { verifier, challenge };
}

// 認可リクエスト送信時(簡易例)
const { verifier, challenge } = await generatePKCE();
// ローカルストレージにVerifierを保存(トークン交換時に使用)
localStorage.setItem(‘pkce_verifier’, verifier);

// 認可サーバーへのURLに challenge と method を含める
// ?code_challenge=…&code_challenge_method=S256

トークン交換の際は、保存しておいた pkce_verifier を送信するだけでいい。これだけで、認可コードは「あなた」しか使えない強力なパスポートに昇格する。

—

4. インフラ・セキュリティ担当者へのアドバイス

アプリ開発者だけでなく、インフラ側も「入り口」で防御を固める必要がある。特にWAF(Web Application Firewall)の設定は重要だ。

Nginx/WAFでの推奨設定

認可コードを含むリクエストに対し、不審なリダイレクト先や、PKCEパラメータを欠いたリクエストを検知する設定を検討してほしい。

Nginxでのリクエストフィルタリング例
location /callback {
# 認可コードの長さや形式を検証(異常に短い/長いものは拒否)
if ($arg_code = “”) {
return 403; # コードがないリクエストは即座に遮断
}

# 認可コードがURLパラメータで漏洩するのを防ぐため、
# 認可サーバー側の設定で State パラメータを必須化し、CSRF対策も徹底する
}

—

まとめ:セキュリティは「性悪説」で設計せよ

「PKCEを実装するのは面倒だ」という現場の声を聞くことがある。しかし、ユーザーのアクセストークンを奪われることの代償は、実装コストの数千倍だ。

1. クライアントタイプに関わらずPKCEを強制する: 最近のOAuth 2.1ドラフトでは、PKCEがデフォルトになっている。
2. 認可サーバー側で検証を厳格化する: code_challenge_method=S256 以外は受け付けないポリシーを徹底すること。

セキュリティに「過剰」はない。今日から君たちのコードベースにPKCEが組み込まれているか、再確認してほしい。それが、プロのエンジニアとしての最低限の責任だ。

もし実装で詰まったら、いつでも相談してくれ。バグを見つけることより、バグを生まない設計をすることが、我々シニアエンジニアの真の仕事なのだから。

コメント

タイトルとURLをコピーしました