こんにちは!セキュリティの世界へようこそ。
普段、ペネトレーションテスト(疑似攻撃テスト)で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は、非常に強力で便利な「鍵」の仕組みです。
しかし、どんなに頑丈な鍵を作っても、受け取る側の「確認プロセス」が抜けていれば、攻撃者は易々とドアを開けて侵入してきてしまいます。
セキュリティの対策は、決して怪しい魔法ではありません
コメント