OAuth 2.0の「Stateパラメータ」を疎かにするな:CSRFを許さないための実装論
現場でコードレビューをしていると、OAuth 2.0の実装で「とりあえず動くから」とstateパラメータを省略したり、固定値を入れていたりするエンジニアに時折出会う。正直に言おう。それは、あなたのアプリケーションの玄関の鍵を、わざわざ相手に手渡しているのと同じだ。
今日は、OAuth 2.0におけるstateパラメータの正体と、それがなぜ「CSRF(クロスサイトリクエストフォージェリ)」という悪夢を食い止める唯一の防波堤なのか、泥臭い実務の視点から解説する。
—
1. なぜ「state」がないと攻撃者が笑うのか?
OAuthの認可フローにおいて、ユーザーは「認可サーバー(GoogleやGitHubなど)」へリダイレクトされ、そこで認証後に「自社アプリのコールバックURL」へ戻ってくる。
攻撃者が狙うのは、この「戻ってきたタイミング」だ。
攻撃シナリオ(PoC)
1. 攻撃者は、自身が認可サーバーでログインし、得られた「認可コード(Authorization Code)」を含むコールバックURLを手に入れる。
2. 攻撃者は、被害者を罠サイトへ誘導し、被害者のブラウザから「攻撃者が用意したコード」を、被害者のセッションとして自社アプリへ送りつける。
3. アプリ側は、誰が送ってきたコードかなんてお構いなしに、そのコードを使ってアクセストークンを取得してしまう。
4. 結果、被害者のアカウントが、攻撃者の意図するIDと紐付けられてしまう(アカウント乗っ取りの完成)。
これが、stateパラメータなしでログイン機能を実装した場合のCSRF攻撃だ。stateは、「今リクエストを送った本人と、コールバックを受け取った本人が、同一人物であること」を証明するための「使い捨ての署名」なのだ。
—
2. 実務で使えるセキュアな実装(PHP編)
stateの要件はたった一つ。「推測不可能(予測不能)なランダム値」であることだ。これをセッションに保存し、コールバック時に照合する。
リクエスト生成時
‘YOUR_CLIENT_ID’,
‘response_type’ => ‘code’,
‘redirect_uri’ => ‘https://yourapp.com/callback’,
‘state’ => $state, // これが重要
]);
header(“Location: ” . $auth_url);
exit;
コールバック検証時
3. セキュリティチーフからの「盲点」への指摘
実装はこれで十分か? いや、まだだ。現場でよくあるミスをいくつか指摘しておく。
- 推測可能な値を使わない:
uniqid()やrand()は論外だ。必ずrandom_bytes()やcrypto.getRandomValues()のような、暗号論的擬似乱数生成器(CSPRNG)を使うこと。 - 「比較」は定数時間で行う: PHPの
hash_equals()を使う癖をつけよう。文字列比較でタイミング攻撃を許さないためだ。 - stateのライフサイクル:
stateは「一度使ったら終わり」だ。検証が成功したか失敗したかに関わらず、セッションから削除するフローを徹底せよ。 - WAFでの補完:
stateパラメータが空の状態でアクセスしてくる不審なリクエストは、WAF(AWS WAFやCloudflareなど)のルールで、特定のパスへのアクセスを遮断または監視する設定を入れるのが定石だ。
参考:Nginxで特定のパスへの異常なリクエストをログに出す設定例
location /callback {
# クエリ文字列にstateが含まれていない場合、カスタムログでマークする
if ($arg_state = “”) {
set $suspicious_request 1;
}
# ログフォーマットに $suspicious_request を含めて監視する
…
}
—
最後に:防御は「疑うこと」から始まる
セキュリティエンジニアとして多くのインシデントを見てきたが、被害を受けるシステムの多くは「機能要件」を優先し、「検証要件」を省略した場所から崩壊する。
stateパラメータは単なる文字列ではない。それは、「このリクエストは、間違いなく私のアプリが開始したプロセスである」という信頼の証だ。
今日からあなたのコードを見直してほしい。stateを検証していないなら、それは「鍵のついていない金庫」だ。今すぐ実装を修正し、ユーザーの信頼を守り抜いてくれ。それが我々エンジニアの矜持だ。
コメント