おい、最近のコードレビューでまた見つけたぞ。認証トークンに JWT (JSON Web Token) を使っているシステムで、署名の検証をサボっていたり、アルゴリズムの固定化(Algorithm Confusion)対策が抜けている実装が多すぎる。
「ライブラリがよしなにやってくれるだろう」なんて甘い考えで設計していると、攻撃者に裏口をこじ開けられ、全ユーザーの権限を奪われるのは時間の問題だ。特にステートレスな認証の要であるJWTは、一度実装を誤ると致命的なセキュリティインシデントに直結する。
今回は、数々のインシデント現場を踏んできた俺が、JWTの安全な署名検証と、世間を騒がせた none アルゴリズムの罠、そして公開鍵管理のリアルな鉄則を叩き込んでやる。手を動かすエンジニアなら、明日から自分のコードをどう直すべきか、最後まで読めば痛いほどわかるはずだ。
—
1. JWTが狙われる理由:なぜ攻撃者は署名を迂回できるのか?
JWTは、ヘッダー(Header)、ペイロード(Payload)、署名(Signature)の3つのパートがドット(.)で連結された非常にシンプルな構造をしている。
header.payload.signature
開発者が最も勘違いしやすいのが、「ペイロードはBase64Urlエンコードされているだけで、暗号化されているわけではない」という点だ。中身は誰でもデコードして丸見えである。そのため、機密情報をペイロードに含めること自体がナンセンスなのだが、それ以上に深刻なのが「署名の検証プロセスにおける実装ミス」だ。
攻撃者が狙う主な脆弱性パターンは以下の2つだ。
1. none アルゴリズムの許容:
ヘッダーの alg(Algorithm)パラメータに none を指定することで、署名検証を無効化し、任意のペイロード(例:isAdmin: true に改ざんしたもの)をシステムに送り込む手法。
2. アルゴリズムの混同(Algorithm Confusion / Key Confusion):
非対称暗号(RSAやECC)の公開鍵を、対称鍵(HMAC / HS256)の「シークレットキー」として誤認させる攻撃。攻撃者は公開鍵を使って自前で有効なHMAC署名を作り、検証側を突破してしまう。
—
2. 脆弱な実装の何が危ないのか?(実例とリスク)
まずは、よくある「やってはいけない実装」を見てみよう。以下のNode.js(jsonwebtoken)のコードは、セキュリティレビューで一発レッドカードを食らう典型例だ。
【危険な実装例】
const jwt = require('jsonwebtoken');
// ❌ 危険:アルゴリズムの検証を行わず、渡されたトークンをそのまま信頼している
function dangerousVerify(token, secretOrPublicKey) {
try {
// algorithmsオプションを指定していないため、
// 攻撃者がヘッダーを改ざんして攻撃を仕掛けられる余地がある
const decoded = jwt.verify(token, secretOrPublicKey);
return decoded;
} catch (err) {
return null;
}
}
このコードの何がまずいかというと、algorithms のホワイトリスト指定を怠っている点だ。これだと、攻撃者がRSAの公開鍵を入手できる環境(例えばHTTPSの証明書やJWKSエンドポイント)において、アルゴリズムを HS256 に書き換え、RSAの公開鍵を「HMACのシークレット」として使って署名を偽造できてしまう。
—
3. 完全防御のためのセキュアな実装サンプル(Node.js / Python)
では、実務でどう書くべきか。ここでは、アルゴリズムの厳格な固定化、none の完全拒否、そして適切な公開鍵管理を盛り込んだセキュアな実装を提示する。
Node.js(jsonwebtoken)によるセキュアな検証実装
非対称暗号(RS256)を使用する場合の、模範的なコードがこれだ。
const jwt = require('jsonwebtoken');
const fs = require('fs');
// 信頼できる公開鍵をファイルシステム等から安全に読み込む
const publicKey = fs.readFileSync('/path/to/public.pem', 'utf8');
function secureVerifyJWT(token) {
try {
const verifiedPayload = jwt.verify(token, publicKey, {
// 【最重要】使用を許可するアルゴリズムを明示的にホワイトリスト化する
// これにより、noneアルゴリズムや意図しない暗号方式を完全にシャットアウトする
algorithms: ['RS256'],
// 発行者(Issuer)の検証
issuer: 'https://auth.example.com',
// 聴衆(Audience)の検証
audience: 'https://api.example.com',
// 有効期限(exp)の厳密なチェック(デフォルトで有効だが明示すると吉)
maxAge: '1h'
});
return {
success: true,
data: verifiedPayload
};
} catch (err) {
// ログには詳細を記録しつつ、クライアントには汎用的なエラーを返す
console.error(`JWT Verification Failed: ${err.message}`);
return {
success: false,
error: 'Invalid or expired token.'
};
}
}
Python(PyJWT)によるセキュアな検証実装
Pythonの PyJWT を使う場合も同様だ。デフォルトの挙動に頼らず、許容するアルゴリズムを明示的に指定する必要がある。
import jwt
from jwt.exceptions import PyJWTError
# 信頼できるRSA公開鍵
PUBLIC_KEY = """
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
-----END PUBLIC KEY-----
"""
def secure_verify_jwt(token: str):
try:
# 署名の検証とクレームのチェックを同時に行う
payload = jwt.decode(
token,
PUBLIC_KEY,
# 【最重要】RS256のみを許可(HS256やnoneを一切許さない)
algorithms=["RS256"],
# クレームの検証設定
options={
"verify_signature": True,
"verify_exp": True,
"verify_iss": True,
"verify_aud": True,
},
issuer="https://auth.example.com",
audience="https://api.example.com",
)
return {"success": True, "data": payload}
except PyJWTError as e:
# 偽造トークンや期限切れ、改ざんはすべてここでキャッチする
print(f"Security Alert - JWT Error: {str(e)}")
return {"success": False, "error": "Authentication failed."}
—
4. 現場で絶対に守るべき運用・設計の鉄則
コードレベルの対策だけでは、インフラや鍵のライフサイクル管理に穴があれば意味がない。最後に、シニアチーフとしてチームに必ず徹底させている運用ルールを共有しよう。
1. none アルゴリズムの処理をライブラリレベルで禁止する
自前でJWTのパーサを書くような愚行は絶対に避け、セキュリティパッチが継続的に適用されている信頼性の高いオープンソースライブラリ(jsonwebtoken, PyJWT, go-jose 等)を使用すること。その上で、常に algorithms パラメータを明示する習慣をつけろ。
2. 公開鍵(JWKS)のキャッシュとローテーション
RSAやECCの公開鍵をハードコーディングするのは論外だ。Auth0やAWS Cognito、自前のIdPが提供する JWKS (JSON Web Key Set) エンドポイントを利用し、定期的に公開鍵を自動取得・キャッシュする仕組みを構築せよ。鍵のローテーション(漏洩時の迅速な無効化)ができないシステムは、セキュリティ設計の欠陥と言わざるを得ない。
3. ペイロードには機密情報を載せない原則の徹底
前述の通り、JWTは暗号化されていない。パスワードハッシュ、個人情報(PII)、クレジットカード情報などをペイロードに含めることは、データベースのダンプよりも容易に情報をばら撒く行為に等しい。識別子(sub や user_id)と権限の最低限のスコープだけを載せるのが鉄則だ。
—
まとめ
JWTは正しく使えば非常に強力でスケーラブルな認証基盤になるが、一歩間違えれば「攻撃者にフリーパスを渡す道具」に成り下がる。
「動けばいい」という実装から脱却し、「アルゴリズムのホワイトリスト化」「noneアルゴリズムの排除」「厳格なクレーム(iss, aud, exp)の検証」の3点セットを、今日から君のプロジェクトの標準仕様として組み込んでほしい。
セキュリティは細部に宿る。妥協のないコードで、セキュアなシステムを作り上げていこう。
コメント