こんにちは!セキュリティの世界へようこそ。
日々の開発やインフラ構築、本当にお疲れ様です。
今回は、現代のWebアプリケーション開発においてなくてはならない存在となった JWT(JSON Web Token) についてお話しします。「名前は聞いたことがあるけれど、実際どうやって安全に扱えばいいの?」と不安に感じている新人エンジニアの方も多いのではないでしょうか。
今回は、家の鍵の防犯にたとえながら、JWTの安全な署名検証と、絶対にやってはいけない「アルゴリズムの固定化(脆弱性)」の対策について、一歩ずつ優しく紐解いていきましょう!
—
1. JWTってなに? 家の「パスポート付き合鍵」で考えてみよう
Webサイトにログインしたとき、「私は〇〇です」という証明書をブラウザとサーバーの間でやり取りする必要がありますよね。この身分証明書として非常によく使われるのがJWTです。
JWTの構造は、大きく分けると次の3つのパーツ(部屋)でできています。
1. Header(ヘッダー): 「何の鍵を使って封印したか(アルゴリズム)」が書かれています。
2. Payload(ペイロード): 「誰がログインしているか(ユーザーIDや有効期限)」という中身(クレーム)です。
3. Signature(シグネチャ): 中身が途中で悪意ある第三者に書き換えられていないかを証明する「封蝋(ふうろう・ワックスシール)」です。
泥棒(攻撃者)の手口:合鍵の偽造
ここで想像してみてください。あなたが家を出るとき、しっかりとした鍵(シグネチャ)をかけたはずなのに、空き巣が勝手に合鍵を作り、「私は家主です!」と言って勝手に入ってきたら大変ですよね。
JWTの世界でも同じことが起きます。
攻撃者は、あなたが送ったJWTのペイロード(中身)を勝手に書き換え、「私は管理者(admin)です!」と改ざんします。もしサーバー側が「おっ、中身が書き換えられているな」と気づけなければ、システム全体の乗っ取りを許してしまうのです。
これを防ぐのが、JWTの最後にくっついている「署名検証」という仕組みになります。
—
2. 恐怖の none アルゴリズムと「アルゴリズムの固定化」
さて、ここからが本題であり、セキュリティ担当者が夜も眠れなくなるポイントです。
JWTのヘッダーには、そのトークンをどんな方法で封印したかを表す alg(アルゴリズム)という項目があります。例えば、しっかりと安全な公開鍵暗号や共通鍵暗号を使っている場合は、alg に RS256 や HS256 といった名前が入ります。
しかし、初期の仕様や一部のライブラリには、次のような恐ろしい「抜け道(仕様の罠)」が存在していました。
> 「鍵を作るのが面倒ですか? それとも検証をスキップしますか? じゃあ alg に none(署名なし)を指定すれば、署名チェックしませんよ!」
これを利用したのが「アルゴリズムの固定化攻撃(noneアルゴリズム攻撃)」です。
攻撃のシナリオ
1. 攻撃者が自分のJWTを作り、ペイロード部分を「管理者権限」に書き換えます。
2. ヘッダーの alg を強引に none(署名なし)に書き換えます。
3. シグネチャ(封印のワックス)を丸ごと削除して、サーバーに送りつけます。
4. 脆弱なサーバー:「お、ヘッダーに none って書いてあるから、今回は署名チェックなしで通そう!」→ 侵入成功!
家で例えるなら、泥棒が「今回は鍵をかけなくていいルールに勝手に変えたので、そのまま入りますね」と言って、鍵穴のないドアを勝手に開けて入ってくるようなものです。恐ろしい話ですよね。
—
3. 実践!安全なJWT検証コードを書こう
「じゃあ、どうやって身を守ればいいの?」という話ですね。
ここからは、実務で使える具体的な対策をコードで見ていきましょう。今回は、Node.jsでよく使われる jsonwebtoken ライブラリを例にします。
ライブラリを使うときは、「どのアルゴリズムを許可するか(ホワイトリスト方式)」を厳格に指定することが鉄則です。
❌ 危ない書き方(避けるべき実装)
アルゴリズムの指定をサボったり、デフォルトのままで実装すると、none アルゴリズムや予期せぬアルゴリズムを受け入れてしまう原因になります。
// 【危険】アルゴリズムを限定せず、送られてきたヘッダーをそのまま信用してしまう例
const jwt = require('jsonwebtoken');
function verifyTokenUnsafe(token, secretKey) {
// 危険:algパラメータの検証が甘く、none攻撃を受ける可能性がある
return jwt.verify(token, secretKey);
}
⭕ 安全な書き方(推奨される実装)
許容するアルゴリズムを明示的に配列(ホワイトリスト)で指定し、none などを絶対に弾く設定にします。
// 【安全】アルゴリズムを厳格に固定した検証の例
const jwt = require('jsonwebtoken');
function verifyTokenSecure(token, publicKeyOrSecret) {
try {
// 許可するアルゴリズムを明確に「RS256」のみに制限する(ホワイトリスト方式)
const options = {
algorithms: ['RS256'], // ここに 'none' や危ないアルゴリズムを含めない!
maxAge: '1h' // ついでにトークンの有効期限(クレーム)も厳しくチェック
};
// 署名の検証とクレーム(有効期限など)の検証を同時に行う
const decoded = jwt.verify(token, publicKeyOrSecret, options);
return { success: true, data: decoded };
} catch (error) {
// 署名が不正な場合や、改ざんされている場合はここで例外をキャッチ
console.error('JWT検証エラー:', error.message);
return { success: false, error: '不正なトークンです。' };
}
}
—
4. クレーム検証と公開鍵の適切な管理
アルゴリズムの固定化を防いだだけでは、まだ完璧とは言えません。
JWTの中身(ペイロード)に含まれるクレーム(Claims)の検証も、セキュリティの要となります。
① 有効期限(exp)の必ずのチェック
JWTは一度発行されると、サーバー側で強制失効させるのが難しいという特徴があります(ステートレスなため)。そのため、短い有効期限(exp: Expiration Time)を必ず設定し、期限切れのトークンは容赦なく拒否(TokenExpiredError)する仕組みが不可欠です。
② 発行者(iss)や対象者(aud)の検証
「このトークンは本当にうちの認証サーバーが発行したものか?(iss)」、「このAPIを叩くためのものか?(aud)」をチェックすることで、他の無関係なシステムで発行されたJWTを悪用される「Confused Deputy(混乱した代理)問題」を防ぎます。
③ 公開鍵の管理(RSAやECCを使う場合)
共通鍵暗号(HS256など)を使う場合、サーバーとクライアント(あるいは複数のマイクロサービス間)で同じ秘密の合言葉を共有する必要があります。もし1つのサービスが破ら sociales、すべてが危険にさらされます。
そのため、本番環境では公開鍵暗号(RSAや楕円曲線暗号 ECC / RS256やES256)を使い、次のように役割を分けるのがベストプラクティスです。
- 認証サーバー(秘密鍵): トークンに厳重に署名(封印)する。秘密鍵は絶対に外に漏らさない。
- 各APIサーバー(公開鍵): 配られた公開鍵を使って、「この署名は本物か?」をチェックするだけ(書き換えはできない)。
—
5. まとめ:一歩ずつ安全なシステムを作っていこう
今回は、JWTの安全な署名検証と、アルゴリズムの固定化対策についてお伝えしました。
1. none アルゴリズムを絶対に許さない(アルゴリズムをホワイトリストで明示的に指定する)。
2. 署名だけでなく、有効期限などのクレームも必ず検証する。
3. 適切な暗号方式(RSAやECCなど)を選び、鍵を厳重に管理する。
セキュリティの対策は、最初は難しく感じるかもしれません。でも、一つひとつの仕組みを「家の防犯」にたとえて紐解いていけば、決して越えられない壁ではありません。
今日からあなたの書くコードに algorithms: ['RS256'](または適切なアルゴリズム)の指定を取り入れて、より堅牢で信頼されるエンジニアへの一歩を踏み出してみませんか? 一緒に安全なWebの未来を作っていきましょう!
コメント