【入門編】 OAuth 2.0のステートパラメータ欠如によるCSRF攻撃 – オフェンシブセキュリティ & リバースエンジニアリング防御ガイド

OAuth 2.0 の「鍵」をかけ忘れると、泥棒が勝手に入ってくる!? ~ state パラメータ欠如による CSRF 攻撃を解き明かす ~

皆さん、こんにちは!サイバーセキュリティの世界へようこそ。今回は、皆さんが普段何気なく使っているかもしれない、でも実はとっても危ない落とし穴、「OAuth 2.0 の state パラメータ欠如による CSRF 攻撃」について、泥棒さんと鍵に例えながら、と~っても優しく解説していきますね。

「え、OAuth 2.0? CSRF? 何それ、難しそう…」と思ったあなた、大丈夫です!この記事を読み終わる頃には、「なるほど!そういうことだったのか!」と膝を打つこと間違いなし。新人IT担当者さんや、セキュリティに初めて触れる開発者さん、そして「なんかよくわかんないけど、とりあえず安全にはしたい!」という全ての皆さんに、分かりやすく、そして実践的に、この攻撃のメカニズムと、どうすれば防げるのかをお伝えします。

そもそも OAuth 2.0 って何? ~ 家の鍵を貸すイメージ ~

まずは、OAuth 2.0 がどんなものか、イメージを掴みましょう。

皆さんも、SNSアカウント(例えば Google や Twitter)を使って、別のサービスにログインした経験があるのではないでしょうか? 「Google でログイン」とか、「Facebook でログイン」ってボタン、見たことありますよね。あれが、OAuth 2.0 の代表的な使われ方です。

これは例えるなら、「あなたの家の鍵を、一時的に信頼できる友人に預ける」ようなものです。

  • あなた(ユーザー): 家の持ち主
  • ログインしたいサービス(クライアントアプリケーション): 鍵を借りたい友人
  • サービスプロバイダー(Google, Twitter など): 家の持ち主(あなた)の信頼性を証明してくれる人(警察署とか、大家さんみたいなイメージ)

本来、ログインしたいサービス(友人)は、あなた(家の持ち主)に「このサービス(例: あなたのプロフィール情報)を見てもいいですか?」と許可を求めます。そして、サービスプロバイダー(警察署)が「はい、この人は確かにこの家の持ち主(あなた)です!」と証明してくれて、初めてログインしたいサービス(友人)が、あなた(家の持ち主)の代わりに、サービスプロバイダー(警察署)から情報(例: プロフィール情報)を受け取って、ログインが完了する。そんな流れなんです。

この「許可を求める」プロセスで、重要な役割を果たすのが、今回のお話の主役、state パラメータなんです。

state パラメータとは? ~ 鍵に「誰が」借りるかの目印をつける ~

では、この state パラメータ、一体何のためにあるのでしょうか?

これは、先ほどの鍵の例えで言うと、「あなたが鍵を貸すときに、『この鍵は〇〇さん(友人の名前)が借りるためのものだよ』という目印(タグ)を鍵につける」ようなものです。

具体的には、OAuth 2.0 のフローで、ユーザーが「Googleでログイン」ボタンを押したとき、サービスプロバイダー(Google)に「このユーザーを認証して、このサービスに情報を渡してもいいですか?」というリクエストを送ります。このリクエストに、ユニークな state という値を一緒につけて送るんです。

この state の値は、ログインしたいサービス(友人)が、「私がリクエストしたやつだ!」と、後でちゃんと確認するための「本人確認のしるし」なんですね。

state パラメータがないと、何が起こる? ~ 泥棒が「鍵を貸して」と偽る! ~

さて、ここからが本題です。もし、この state パラメータをつけずに、鍵を貸し出しちゃったらどうなるでしょうか?

それは、まるで、「誰でもいいから、うちの鍵を勝手に持っていっていいですよ!」と言っているようなもの。これでは、泥棒さんが「鍵を借りたいんです!」と言ってきたときに、本当はあなた(家の持ち主)が許可したはずの友人ではなく、泥棒さん(悪意のある第三者)が、その鍵を勝手に持っていけてしまうんです!

これが、OAuth 2.0 の文脈でいう CSRF(Cross-Site Request Forgery:クロスサイトリクエストフォージェリ)攻撃 に繋がります。

攻撃者は、あなたを騙して、本来であればあなただけがアクセスできるはずの、サービスプロバイダー(Googleなど)の認証ページに、「あなた(ユーザー)がログインするふり」をさせて、不正なリクエストを送信します。

具体的には、こんな流れで攻撃が進む可能性があります。

1. 攻撃者は、巧妙に作られたウェブサイトやメールを用意します。

  • このウェブサイトには、あなたの個人情報(例えば、あなたが普段使っているサービス)が勝手に紐づけられた、不正なOAuth 2.0の認証リクエストが仕込まれています。

2. あなたは、そのウェブサイトを見てしまったり、メールのリンクをクリックしてしまいます。

  • すると、あなたのブラウザは、意図せず、サービスプロバイダー(Googleなど)の認証ページに飛ばされます。

3. サービスプロバイダー(Google)は、あなたのブラウザから「認証して!」というリクエストを受け取ります。

  • しかし、ここが問題!もし、このリクエストに state パラメータがついていなければ、サービスプロバイダー(Google)は、「これは本当に、あなたがログインしたいサービスからのリクエストなのか?」を判断できません。

4. サービスプロバイダー(Google)は、認証が成功したと勘違いし、攻撃者が用意した「コールバックURL」(認証後に戻ってくる場所)に、あなたに代わって、不正な情報を送り返してしまいます。

  • この情報には、攻撃者があなたに代わってサービスにアクセスできるための「アクセストークン」などが含まれている可能性があります。

5. 攻撃者は、このアクセストークンを使って、あなたの情報に不正にアクセスしたり、あなたの代わりにサービスを利用したりできるようになってしまうのです!

まるで、泥棒が、あなたの家の鍵を借りるふりをして、実際には自分のために鍵を複製してもらったような状態ですね。恐ろしい…。

なぜ state パラメータが重要なのか? ~ 鍵の「誰用」タグをチェックする ~

では、この state パラメータがあれば、どうして防げるのでしょうか?

これは、鍵を貸すときに「この鍵は〇〇さん(友人の名前)が借りるためのものだよ」という目印(タグ)をつけたことを、後でちゃんと確認する、というイメージです。

1. あなたがログインしたいサービス(友人)は、サービスプロバイダー(Googleなど)にリクエストを送る際に、ランダムでユニークな state の値を生成し、一緒に送ります。

  • https://example.com/auth/google?client_id=YOUR_CLIENT_ID&redirect_uri=YOUR_REDIRECT_URI&response_type=code&scope=profile&state=a1b2c3d4e5f6g7h8i9j0

2. サービスプロバイダー(Google)は、あなたを認証した後、この state の値を、あなたがログインしたいサービス(友人の住所)に送り返します。
3. あなたがログインしたいサービス(友人)は、受け取った state の値が、最初に自分で生成して送った state の値と「一致するか」を必ず確認します。

  • もし一致すれば、「お、これは私がリクエストしたやつだ!」と判断し、正規の処理(アクセストークンを発行するなど)に進みます。
  • しかし、もし一致しなければ、「あれ?これは私がリクエストしたのとは違う state だぞ?誰かが勝手に送ってきたんじゃないか?」と判断し、処理を中断します。

このように、state パラメータは、「認証リクエストと、その後のコールバック(認証完了後の戻り先)で、送られてきた情報が本当に自分がリクエストしたものと一致しているか」を確認するための、非常に重要な「整合性チェック」の役割を果たしているんです。

攻撃を防ぐための「戸締り」 ~ 実践的な防御策 ~

では、開発者として、この state パラメータの欠如による CSRF 攻撃を防ぐには、どうすれば良いのでしょうか?

これは、家の戸締りをしっかりする、というイメージです。

1. state パラメータを必ず生成し、設定する

まず、ユーザーが OAuth 2.0 でログインする際、必ずユニークで予測不可能な state パラメータを生成し、サービスプロバイダーへのリクエストに含めるようにしましょう。

多くの OAuth 2.0 ライブラリやフレームワークには、この state パラメータを自動生成してくれる機能があります。それらを活用するのが一番簡単で安全です。

例えば、PHP でよく使われる league/oauth2-client ライブラリを使った場合のイメージはこんな感じです。

<?php
require 'vendor/autoload.php'; // Composer のオートローダーを読み込む

// OAuth 2.0 クライアントの設定
$clientId     = 'YOUR_CLIENT_ID';
$clientSecret = 'YOUR_CLIENT_SECRET';
$redirectUri  = 'YOUR_REDIRECT_URI'; // コールバックURL

// プロバイダーのインスタンスを作成 (例: Google)
$provider = new \League\OAuth2\Client\Provider\Google([
    'clientId'     => $clientId,
    'clientSecret' => $clientSecret,
    'redirectUri'  => $redirectUri,
]);

// 認証リクエストの開始
// ここで、ライブラリが自動的にユニークな 'state' パラメータを生成してくれます!
$authorizationUrl = $provider->getAuthorizationUrl();

// 生成された 'state' パラメータをセッションに保存
// 後でコールバック時に照合するために必要です
$_SESSION['oauth2state'] = $provider->getState();

// ユーザーを Google の認証ページにリダイレクト
header('Location: ' . $authorizationUrl);
exit;
?>

このコードでは、$provider->getAuthorizationUrl() を呼び出す際に、ライブラリが自動的にランダムな state パラメータを生成し、URL に付与してくれます。そして、その生成された state の値を $_SESSION['oauth2state'] に保存しています。

2. コールバック時に state パラメータを照合する

次に、ユーザーがサービスプロバイダーでの認証を終えて、あなたのサービス(redirectUri で指定したURL)に戻ってきたとき、受け取った state パラメータが、最初に保存しておいたものと一致するかを必ず確認します。

<?php
// session_start() を呼び出していることを確認してください
session_start();

require 'vendor/autoload.php'; // Composer のオートローダーを読み込む

// OAuth 2.0 クライアントの設定 (上記と同じ設定)
$clientId     = 'YOUR_CLIENT_ID';
$clientSecret = 'YOUR_CLIENT_SECRET';
$redirectUri  = 'YOUR_REDIRECT_URI';

// プロバイダーのインスタンスを作成 (例: Google)
$provider = new \League\OAuth2\Client\Provider\Google([
    'clientId'     => $clientId,
    'clientSecret' => $clientSecret,
    'redirectUri'  => $redirectUri,
]);

// 1. セッションに保存した 'state' が存在するか確認
if (empty($_SESSION['oauth2state'])) {
    // セッションに 'state' がない場合、エラー処理
    echo 'セッションに state が保存されていません。再度ログインを試みてください。';
    exit;
}

// 2. ユーザーから送信された 'state' と、セッションに保存した 'state' を比較
if (!isset($_GET['state']) || $_GET['state'] !== $_SESSION['oauth2state']) {
    // 'state' が一致しない場合、CSRF攻撃の可能性あり!処理を中断。
    unset($_SESSION['oauth2state']); // セッションの 'state' をクリア
    echo '無効なリクエストです。state パラメータが一致しません。';
    exit;
}

// 3. 'state' が一致した場合、アクセストークンを取得する処理へ進む
try {
    // アクセストークンを取得
    $accessToken = $provider->getAccessToken('authorization_code', [
        'code' => $_GET['code'], // 認証コード
    ]);

    // ここで取得した $accessToken を使って、ユーザー情報を取得したり、APIを呼び出したりできます。
    echo '認証に成功しました!アクセストークン: ' . $accessToken->getToken();

    // セッションから 'state' をクリア
    unset($_SESSION['oauth2state']);

} catch (\League\OAuth2\Client\Provider\Exception\IdentityProviderException $e) {
    // エラー処理
    echo '認証中にエラーが発生しました: ' . $e->getMessage();
}
?>

このコールバック処理では、

1. まず、セッションに state が保存されているかを確認します。
2. 次に、ユーザーがブラウザから送ってきた state パラメータ ($_GET['state']) と、セッションに保存しておいた $_SESSION['oauth2state'] を比較します。
3. もし一致しない場合は、CSRF攻撃の可能性が高いと判断し、以降の処理を中断します。 これで、不正なリクエストからあなたのサービスを守ることができます。
4. 一致した場合のみ、正規のアクセストークン取得処理に進みます。

まとめ ~ セキュリティは、日々の「戸締り」から ~

いかがでしたでしょうか? OAuth 2.0 の state パラメータは、一見地味だけれど、CSRF 攻撃を防ぐための、まさに「家の鍵につける目印」のような、とっても大切な役割を果たしているんですね。

今回の state パラメータの件に限らず、セキュリティ対策というのは、日々の開発や運用の中で、「これって本当に安全かな?」と立ち止まって考え、適切な「戸締り」をすることが大切なんです。

  • 「鍵を渡すときは、誰に渡すかちゃんと確認する」
  • 「使わない鍵は、ちゃんと返してもらう(セッションから state をクリアする)」

こういった、ちょっとした「当たり前のこと」を、一つ一つ丁寧に行うことが、皆さんのサービスを、そしてユーザーの情報を守ることに繋がります。

もし、皆さんの開発しているアプリケーションで OAuth 2.0 を使っているのに、state パラメータの設定や照合をしっかり行っていなかったら、ぜひ今日お伝えした内容を参考に、見直しをしてみてくださいね。

セキュリティの世界は、常に新しい脅威が現れますが、基本をしっかり押さえることで、多くの攻撃から身を守ることができます。これからも、一緒に一歩ずつ、安全なシステム開発を目指していきましょう!

それでは、また次回のセキュリティ探求でお会いしましょう!

コメント

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