JWTはなぜ「実装の墓場」となったのか
ペネトレーションテストの現場で、モダンなWebアプリケーションやマイクロサービスアーキテクチャのターゲットと対峙する時、私は真っ先に認証・認可レイヤー、特にJSON Web Token(JWT)の実装をスキャンする。ステートレスでスケーラブル、OAuth 2.0やOIDCの文脈でデファクトスタンダードとなったこの仕組みは、残念ながら開発現場の「セキュリティへの過信」が生んだ数々の実装ミスの温床となっている。
仕様書(RFC 7519)自体によくある欠陥があるわけではない。問題は、それを扱うライブラリのデフォルト挙動の甘さと、開発者が「暗号化されている(実際には大半が署名されているだけだ)」という誤った安心感を持つことにある。JWTのペイロードはBase64Urlでエンコードされているだけであり、誰でも一瞬でデコードして中身を覗き見ることができる。そして、その署名の検証ロジックにわずかでも不備があれば、攻撃者は容易にクレーム(claims)を書き換え、システム全体の管理者権限を強奪できる。
今回は、実戦のペネトレーションテストにおいて私が常習的に用いるJWTの脆弱性ハンティング手法と、それらを根本から断つためのアーキテクチャ設計について、プロフェッショナルの視点から赤裸々に解説しよう。
—
1. 攻撃者の視点:実戦におけるJWTバイパス手法
JWTを悪用した認証バイパスは、単にトークンを書き換えて送るだけではない。プロトコルの仕様の隙間、暗号学的アルゴリズムの弱点、そして実装ライブラリのパース処理の差異を巧みに突く必要がある。
アルゴリズムのダウングレード攻撃(Noneアルゴリズム)
最も古典的でありながら、今なお中堅規模の開発現場で散見されるのが、署名検証をスキップさせる alg: "none" の悪用だ。JWTのヘッダーには、使用された署名アルゴリズムを指定する alg パラメータが含まれている。
{
"alg": "none",
"typ": "JWT"
}
もしバックエンドの検証ライブラリが、設定不備によって alg に none(または大文字・小文字を混在させた None, NONE 等)を指定されたトークンを受け入れてしまった場合、署名(Signature)部分が空であっても、サーバー側は改ざんされたペイロードを信頼してしまう。
攻撃者は、デコードしたペイロード内の sub(サブジェクト)を管理者ユーザーのIDに書き換え、exp(有効期限)を未来に設定し、ヘッダーの alg を none に変更して、末尾の署名を除去(または空のまま)してリクエストを投げる。これだけで認証バイパスが成立する。
非対称暗号(RS256)から対称暗号(HS256)へのすり替え
マイクロサービス環境でよく見られるのが、トークンの発行(認証サーバー)には秘密鍵が必要な非対称アルゴリズム(RS256等)を使いつつ、検証側(各リソースサーバー)で共通鍵アルゴリズム(HS256等)も受け入れてしまうという致命的な設定ミスだ。
RS256では、サーバーは「秘密鍵(Private Key)」で署名し、「公開鍵(Public Key)」で検証を行う。しかし、脆弱な実装では、検証時に「どのアルゴリズムを使うべきか」をJWTヘッダーの alg の指定に依存して動的に決めてしまうことがある。
ここで攻撃者は次のようなトリックを使う。
1. 認証サーバーから公開鍵(通常は /.well-known/jwks.json 等で外部公開されている)を入手する。
2. 攻撃者は、自身のローカル環境で、その公開鍵のバイト列自体を「HS256の共通鍵(Secret)」と見なして、改ざんしたペイロードに署名する。
3. リソースサーバーは、ヘッダーに alg: "HS256" が指定されているのを見て、「あ、今回はHS256だな」と判断する。そして、公開鍵を共通鍵として使い、送られてきた署名を検証してしまう。公開鍵自体から計算されたHMACであるため、検証は成功し、認証は完全に突破される。
—
2. 脆弱な実装と安全な実装のコード比較
では、なぜこのような事故が起きるのか。具体的なコードを比較しながら、その根本原因と正しい実装アプローチを確認しよう。
以下の脆弱なNode.js(Express + jsonwebtoken)のコードを見てほしい。
【脆弱な実装例】検証を怠った危険なコード
const jwt = require('jsonwebtoken');
// 危険な検証処理の例
function verifyTokenVulnerable(req, res, next) {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (!token) return res.sendStatus(401);
// 【脆弱性】アルゴリズムのホワイトリストを明示せず、送られてきたヘッダーのalgを信頼している
// さらに、公開鍵を適切に固定していない場合、HS256へのダウングレード攻撃を受ける
jwt.verify(token, process.env.JWT_PUBLIC_KEY, (err, user) => {
if (err) return res.sendStatus(403);
req.user = user;
next();
});
}
この実装の問題点は、jwt.verify に許容するアルゴリズムの制約(algorithms オプション)を渡していない点にある。ライブラリのバージョンや設定によっては、予期せぬアルゴリズムを受け入れてしまう余地を与えてしまう。
【堅牢な実装例】厳格な型チェックとアルゴリズムの固定
真にセキュアな実装では、使用するアルゴリズムをハードコード(または厳密に制限)し、クレームの構造もスキーマ検証で担保する必要がある。
const jwt = require('jsonwebtoken');
// 許可するアルゴリズムを明示的に定数定義
const ALLOWED_ALGORITHMS = ['RS256'];
function verifyTokenSecure(req, res, next) {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'アクセストークンがありません' });
}
const publicKey = process.env.JWT_PUBLIC_KEY;
// 【対策】algorithmsオプションに期待するアルゴリズムのみを配列で強制する
// これにより、HS256やnoneアルゴリズムへのダウングレードを完全に阻止する
jwt.verify(token, publicKey, { algorithms: ALLOWED_ALGORITHMS }, (err, decoded) => {
if (err) {
// セキュリティ上の理由から、詳細なエラー理由はクライアントに返さない
return res.status(403).json({ error: '無効なトークンです' });
}
// クレームの追加検証(必須フィールドの存在確認など)
if (!decoded.sub || !decoded.role) {
return res.status(403).json({ error: 'トークンの構造が不正です' });
}
req.user = decoded;
next();
});
}
このように、algorithms オプションの明示は最低限の防壁である。さらに、鍵のローテーション、JTI(JWT ID)を用いたリプレイ攻撃の防止、有効期限(exp)の厳格なチェック(数秒の猶予すら与えない設計)を組み合わせることで、攻撃の難易度を跳ね上げることができる。
—
3. 次世代の脅威:耐量子暗号(PQC)時代におけるJWTの行方
ペネトレーションテストやセキュリティアーキテクチャの最前線にいる我々が、今目を向けなければならないのは、将来的な「量子コンピューターの脅威」だ。
現在、多くのJWTで署名に使われているRSA(RS256)やECDSA(ES256)といった非対称暗号は、Shorのアルゴリズムを実行可能な十分な量子ビットを持つ量子コンピューターの登場により、近い将来、秘密鍵が多項式時間で算出されるリスク(Q-Day)を抱えている。
もし攻撃者が過去にキャプチャした通信から有効なJWTの署名(RS256等)を蓄積し、将来量子コンピュータを用いて公開鍵から秘密鍵を復元できた場合、過去のトークンの偽造だけでなく、過去のセッションのなりすましがすべて可能になる(Store-Now, Decrypt-Laterの認証版)。
これに対抗するため、NIST(アメリカ国立標準技術研究所)が標準化を進める耐量子暗号(Post-Quantum Cryptography: PQC)アルゴリズムをJWTの署名レイヤーにどう組み込むかという議論が始まっている。例えば、Latticeベース(格子暗号)の署名アルゴリズムをJWTの alg パラメータに適用するためのドラフト仕様や、ハイブリッド署名(従来のECDSA署名とPQC署名の両方を付与する方式)への移行が、今後のエンタープライズアーキテクチャにおける必須要件となってくる。
インフラストラクチャやAPIゲートウェイの設計を担当するテックリードは、いま使っている暗号アルゴリズムが「あと何年耐えられるか」ではなく、「次世代の暗号アジリティ(Crypto-Agility)を考慮したトークン検証基盤になっているか」という視点で、認証ミドルウェアの抽象化レイヤーを見直すべきだ。
—
チーフホワイトハッカーからの提言
JWTは「便利で手軽な仕組み」であるゆえに、セキュリティの基本原則である「入力の信頼性を一切信じるな(Zero Trust)」が忘れられがちだ。
ペネトレーションテストの現場でJWTを見つけたら、私はまず alg: "none" を試し、次に鍵の総当たり(Weak SecretのCracking)、そしてアルゴリズムすり替えを機械的に実行する。そして驚くべきことに、いまだに大企業のAPIの多くがこれらの攻撃に屈する。
コードを書くとき、インフラを組むとき、あるいはアーキテクチャをレビューするとき、自分自身にこう問いかけてほしい。
「このトークンの検証ロジックは、攻撃者が完全にコントロールしたヘッダーとペイロードを送り込んできたとき、完全に沈黙するか?」
その答えが「Yes」である時初めて、あなたのシステムは真の堅牢性を手に入れる。脆弱性の芽を摘むのは、ガイドラインの熟読ではなく、攻撃者の冷徹な眼差しを持ったコードレビューなのだ。
コメント