【入門編】 OpenID ConnectのIDトークン検証不備によるなりすまし – オフェンシブセキュリティ & リバースエンジニアリング防御ガイド

こんにちは!セキュリティの世界へようこそ。

普段、ペネトレーションテスト(疑似攻撃テスト)でWebシステムの脆弱性を調査していると、とてもおもしろい(そして少し恐ろしい)現象によく遭遇します。

それは、「鍵自体は最高級の暗号技術で作られているのに、受け取る側の『確認の仕方がガバガバ』なせいで、泥棒が通り抜け放題になっている」 という事態です。

その代表格が、今回のテーマである 「OpenID Connect(OIDC)のIDトークン検証不備」 です。

一見すると「難しい暗号の用語ばかりで難しそう…」と感じるかもしれません。でも大丈夫です!
今回は、新人IT担当者の方や開発者の方に向けて、「家の鍵」や「テーマパークの通行手形」のような身近な例えを使いながら、攻撃者がどこを狙うのか、そしてどう防ぐのかを分解して解説します。

一歩ずつ対策を学んでいきましょう!

—

そもそも「IDトークン」ってどんなもの?

OpenID Connect(OIDC)の世界では、ユーザーが「私は誰々です」と証明するために 「IDトークン」 というデジタルな通行手形を使います。

この通行手形は、一般的に JWT(JSON Web Token: ジョット) という形式で作られています。中身をわかりやすく例えると、以下のような「カード」です。

+---------------------------------------------------+
|  【ヘッダー】カードの種類や暗号の方式                 |
|  【ペロード】「私は山田太郎です」という情報            |
|  【署名】    発行元(認証サーバー)のデジタルハンコ   |
+---------------------------------------------------+

この通行手形には、主に以下の3つの大切な情報が書いてあります。

1. 発行者(iss / issuer): この手形を誰が発行したか?(例: Google、Auth0など)
2. 宛先・オーディエンス(aud / audience): この手形は「どのアプリ」向けに発行されたか?(例: あなたのWebサービス)
3. 署名(Signature): 手形が途中で書き換えられていない証明(発行元のデジタルハンコ)

普通に考えれば、安全そうですよね?
しかし、アプリ側(検証する側)がこの手形を正しくチェックしないと、泥棒のなりすましを許してしまうのです。

—

泥棒はどうやって侵入する? 3つの「検証不備」と攻撃メカニズム

攻撃者(レッドチーム)がペネトレーションテストで狙う「盲点」は、大きく分けて3つあります。テーマパークの入場ゲートにいる警備員さんを想像しながら読んでみてください。

1. ハンコ(署名)を見ない警備員:署名検証のスキップ

一番マシに見えて、実はめちゃくちゃ多いミスがこれです。

プログラムの中で、JWTの見た目(中身のJSON)だけを解読して「あ、山田太郎さんですね!どうぞ!」と通してしまうパターンです。

  • 泥棒の手段: 自分の名前を「管理者(Admin)」に書き換えた偽造カードを作成します。
  • なぜ破られるか: アプリ側が「デジタルハンコ(署名)が本物か?」を暗号的に計算して確かめる処理をサボっているため、偽造カードだと気づけません。

2. よそのテーマパークの券を通す警備員:iss(発行者)の確認不足

署名のチェックはしっかり行っているとします。「よし、これは信頼できる〇〇認証会社の本当のハンコだ!」と確認しました。

しかし、「どの認証会社が発行したか」 をチェックしていないとどうなるでしょう?

  • 泥棒の手段: 攻撃者は、自分で作った悪意ある認証サーバー(または別の自由に着せ替えできる認証サービス)で本物のハンコを押したカードを作成し、あなたのアプリに提出します。
  • なぜ破られるか: 「信頼できる発行者(iss)」を限定していないため、暗号的に正しいハンコであれば、どんな怪しい発行元からのカードでも受け入れてしまうのです。

3. 隣のライドのチケットで入ってくる客:aud(対象者)の確認不足

これが最も巧妙で、開発者が一番見落としやすい盲点です!

例えば、ある認証サービス(Auth0やGoogleなど)を使って、アプリAとアプリBの2つを運営しているとします。泥棒はアプリAの正規ユーザーですが、アプリBには入れません。

  • 泥棒の手段: 泥棒は「アプリA向けに正常に発行されたIDトークン」を手に入れます。そして、そのトークンをそのまま「アプリB」に提出します。
  • なぜ破られるか: アプリBの警備員は、ハンコも正しい(本物のGoogleの発行)、発行者も正しい(Google)と確認しました。しかし、「このカードはアプリA専用ですよ」という宛先(aud)の確認を忘れていたのです!

結果として、アプリAの権限を使って、アプリBへ「なりすましログイン」が成功してしまいます。

—

攻撃者の視点:なぜこの脆弱性が生まれるのか?

現場の開発者の方とお話しすると、「ライブラリを使って jwt.decode() したから大丈夫だと思っていました!」という声をよく聞きます。

実は、ここに罠があります。

多くのJWTライブラリにある jwt.decode() という関数は、「暗号を検証せず、中身をただ読みやすいテキストに戻すだけ」 の機能であることが多いのです。

一方で、正しく検証を行うには jwt.verify() のような関数を使い、署名鍵・iss・aud の3つをセットで検証しなければなりません。

攻撃者は、開発者がついついやりがちな「手抜き」や「ライブラリの使い方の勘違い」をじっと狙っているのです。

—

実践!安全なトークン検証コード(Node.js編)

では、具体的にどのようにコードを書けば良いのでしょうか?

今回は、Web開発でよく使われる Node.js (jsonwebtoken ライブラリ) を例に、「ダメなコード」 と 「安全なコード」 を見比べてみましょう。

❌ ダメなコード(なりすまし可能!)

const jwt = require('jsonwebtoken');

// 認証ヘッダーから送られてきたトークンを受け取る
function loginHandler(req, res) {
    const token = req.headers.authorization?.split(' ')[1];

    // 【危険!】decodeは中身をパースするだけで、署名もissもaudも検証しません!
    const payload = jwt.decode(token);

    if (payload && payload.sub) {
        // トークンの中身を信じてそのままログインを許可してしまう…
        console.log(`ユーザー ${payload.sub} としてログインしました`);
        res.send("ログイン成功!");
    } else {
        res.status(401).send("認証失敗");
    }
}

⭕️ 安全なコード(堅牢な防御!)

安全な実装では、認証サーバーから公開鍵(JWKS)を取得し、署名・iss・aud をすべて一括で厳格にチェックします。

const jwt = require('jsonwebtoken');
const jwksClient = require('jwks-rsa');

// 1. 信頼できる認証サーバーの鍵取得クライアントを設定
const client = jwksClient({
    jwksUri: 'https://auth.example.com/.well-known/jwks.json' // 信頼するIdPの鍵置き場
});

function getKey(header, callback) {
    client.getSigningKey(header.kid, (err, key) => {
        if (err) return callback(err);
        const signingKey = key.getPublicKey();
        callback(null, signingKey);
    });
}

// ログインハンドラー
function secureLoginHandler(req, res) {
    const token = req.headers.authorization?.split(' ')[1];

    if (!token) {
        return res.status(401).send("トークンがありません");
    }

    // 2. verify関数を使い、署名・iss・audを「すべて」厳密に検証する
    const validationOptions = {
        algorithms: ['RS256'], // 許可する暗号アルゴリズムを明示('none'などを排除)
        issuer: 'https://auth.example.com/', // 【チェック1】発行者(iss)が期待通りか?
        audience: 'my-web-app-client-id'      // 【チェック2】宛先(aud)が自分のアプリ宛か?
    };

    jwt.verify(token, getKey, validationOptions, (err, decoded) => {
        if (err) {
            // 署名が合わない、issが違う、audが違う、有効期限切れ(exp)の場合はすべてここで弾かれます
            console.error("トークン検証エラー:", err.message);
            return res.status(401).send("無効なトークンです");
        }

        // 3. すべての検証をパスした安全なデータのみを使用する
        console.log(`認証成功! ユーザーID: ${decoded.sub}`);
        res.send(`ようこそ、${decoded.sub} さん!`);
    });
}

—

防御のためのチェックリスト

自分のシステムが安全かどうか、今日からできるチェックポイントをまとめました!

  • [ ] jwt.decode() を認証の判断に使っていませんか?
  • 必ず署名検証を行う jwt.verify() などを使いましょう。
  • [ ] iss (発行者) を固定値で検証していますか?
  • 信頼できる認証サーバー(IdP)のURLと完全一致するか確認しましょう。
  • [ ] aud (受信者) に自分のクライアントIDを指定していますか?
  • 他のアプリ向けに発行されたトークンを使い回せないようにしましょう。
  • [ ] 暗号アルゴリズム(alg)を明示的に制限していますか?
  • alg: "none"(署名なし)という危険な設定を受け入れないように RS256 などを明示しましょう。

—

まとめ:正しく「鍵」を改札に引き渡そう

OpenID ConnectやJWTは、非常に強力で便利な「鍵」の仕組みです。
しかし、どんなに頑丈な鍵を作っても、受け取る側の「確認プロセス」が抜けていれば、攻撃者は易々とドアを開けて侵入してきてしまいます。

セキュリティの対策は、決して怪しい魔法ではありません

コメント

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