JWTの「HS256」という甘美な罠 —— なぜ今すぐRS256/ES256へ移行すべきか
現場でコードレビューをしていると、いまだにJWTの署名アルゴリズムに「HS256」を使っているプロダクトを見かける。正直に言おう、それは「家の鍵を合鍵屋で作らず、自作の粘土で型取りしている」ようなものだ。
今日は、JWTの設計思想を根本から見直し、攻撃者に隙を与えない堅牢な実装へと引き上げるための技術論を語る。
なぜHS256は「選んではいけない」のか
HS256(HMAC with SHA-256)は対称鍵暗号だ。つまり、署名する側(サーバー)と検証する側が「同じ一つの秘密鍵」を共有する。
これが何を意味するか。マイクロサービス化が進んだ現代において、複数のサービスがその「秘密鍵」を保持しなければならない。一箇所でも設定ミスでログファイルに秘密鍵が出力されたり、開発者のPCから漏洩したりすれば、その瞬間にシステム全体の認証基盤が崩壊する。
攻撃者が狙う「alg: none」と鍵の強奪
HS256の実装が甘い場合、以下のような脆弱性が即座に突かれる。
1. alg: none 攻撃: JWTヘッダーの alg を none に書き換え、署名部分を削除することで、サーバーが署名を検証せずにトークンを信頼してしまう脆弱性。
2. 鍵の総当たり(Brute-force): HS256の鍵が短かったり、推測可能な文字列だった場合、オフラインで署名を再生成され、管理者権限を捏造される。
非対称鍵(RS256/ES256)への移行が「最強」の理由
非対称鍵方式(RS256やES256)なら、秘密鍵はトークンを発行する「認証サーバー」のみが持ち、検証側は「公開鍵」だけを持てばいい。公開鍵が漏洩しても、攻撃者がトークンを偽造することは不可能だ。これがセキュリティの鉄則である。
実践:セキュアな検証ロジック(Node.js / jsonwebtoken)
多くのライブラリは、デフォルトの設定が非常に危険な状態になっている。以下のコードは、アルゴリズムを強制し、検証プロセスを厳格化した実装例だ。
const jwt = require(‘jsonwebtoken’);
const fs = require(‘fs’);
// 公開鍵を読み込む(検証側はこれだけでいい)
const publicKey = fs.readFileSync(‘./public_key.pem’, ‘utf8’);
function verifyToken(token) {
try {
// 重要なポイント:
// 1. algorithmsで明示的に許可するアルゴリズムを指定する(HS256を排除)
// 2. 署名アルゴリズムを強制し、none攻撃を無効化する
const decoded = jwt.verify(token, publicKey, {
algorithms: [‘RS256’],
issuer: ‘my-auth-server’,
audience: ‘my-app-service’
});
return decoded;
} catch (err) {
// ここでエラーをキャッチし、ログには詳細を出しつつユーザーには汎用的なエラーを返す
console.error(‘JWT検証失敗:’, err.message);
throw new Error(‘Unauthorized’);
}
}
クラウド環境での鍵管理(AWS KMSの活用)
コード内に鍵をハードコードするのは論外だが、サーバーの環境変数に置くのも「中級レベル」だ。真のプロフェッショナルは、AWS KMS(Key Management Service)のようなHSM(ハードウェアセキュリティモジュール)を利用する。
IAMポリシーで「誰が署名できるか」を厳格に制御し、秘密鍵そのものをメモリ上にすら展開させない運用を目指すべきだ。
AWS IAMポリシーの例: 署名に必要な権限のみを最小限に絞る
{
“Version”: “2012-10-17”,
“Statement”: [
{
“Effect”: “Allow”,
“Action”: “kms:Sign”,
“Resource”: “arn:aws:kms:ap-northeast-1:123456789012:key/your-key-id”,
“Condition”: {
“StringEquals”: { “kms:EncryptionAlgorithm”: “RSASSA_PSS_SHA_256” }
}
}
]
}
現場のエンジニアへ送る「チェックリスト」
明日からの開発で、以下の項目を一つずつ潰してほしい。
- [ ] アルゴリズムの固定: ライブラリの
verifyメソッドでalgorithms: ['RS256']を明示しているか? - [ ] 鍵の分離: 認証サーバーとリソースサーバーで秘密鍵を共有していないか?
- [ ] 秘密鍵の保管: コードリポジトリに鍵をコミットしていないか?(
.gitignoreで漏れる事故は後を絶たない) - [ ] 失効リスト(ブラックリスト)の検討: JWTはステートレスだが、ログアウトを実現するためにRedis等で失効トークンを管理しているか?
最後に:セキュリティは「性悪説」で設計せよ
JWTのセキュリティは、攻撃者が「仕様の隙間」を縫ってくることを前提に設計しなければならない。ライブラリがよしなにやってくれることを期待するのではなく、「どのようなアルゴリズムが使われたか」をコードレベルで強制する。これだけで、Webアプリの堅牢性は段違いに向上する。
もし君たちのチームでまだHS256を使っているなら、今日が移行のチャンスだ。面倒な移行作業かもしれないが、大規模なデータ漏洩が発生した後の事後対応に比べれば、遥かに安上がりな投資になるはずだ。
戦い続けろ。コードの守りは、君たちの手の中にしかない。
コメント