なぜ「署名の検証」をサボるのか? JWTのアルゴリズム固定が招く破滅への道
現場でインシデント対応をしていると、決まって「まさかそこを突かれるとは思わなかった」という溜息を耳にする。特にOpenID Connect(OIDC)を利用した認証システムにおいて、IDトークン(JWT)の署名検証ロジックが適当な実装になっているケースは、まさに「玄関の鍵をかけずに警備員を雇っている」のと同じだ。
今回は、JWTの署名アルゴリズムを検証せず、攻撃者に「好きなID」を捏造させる隙を与えてしまう実装の罠と、それを確実に防ぐための「防御の鉄則」を共有する。
攻撃のメカニズム:なぜ alg: none は通ってしまうのか
攻撃者は、JWTが「ヘッダー」「ペイロード」「署名」の3部構成であることを悪用する。多くのライブラリは、受信したトークンのヘッダーを読み取り、そこに指定されたアルゴリズムで署名を検証しようとする。
もし、あなたがライブラリのデフォルト設定を信じ込み、検証アルゴリズムを明示的に固定していない場合、攻撃者は以下のステップで侵入を試みる。
1. トークンの改ざん: ペイロード(subやemail)を、管理者権限を持つユーザーIDに書き換える。
2. アルゴリズムの無効化: ヘッダーの alg パラメータを none に書き換える。
3. 署名の削除: トークンの末尾の署名部分を削り、セパレーターのドット(.)だけを残す。
この状態で、サーバー側が「ヘッダーに none と書いてあるから、署名検証はスキップしよう」と判断してしまえば、ゲームオーバーだ。認証がバイパスされ、攻撃者は任意のユーザーとしてログインできてしまう。
現場で守るべき「ハードコーディング」の鉄則
対策はシンプルだ。「ライブラリにアルゴリズムの判断を委ねないこと」。これに尽きる。
検証ロジックを実装する際は、外部から入力される alg の値を一切信用せず、サーバー側が期待するアルゴリズム(例: RS256)のみをホワイトリストとして固定する。
実装サンプル:Python (PyJWT) での堅牢な検証
Pythonで PyJWT を使う際、最も危険なのは algorithms パラメータを省略することだ。以下のように、厳密に固定する。
import jwt
サーバー側で信頼する公開鍵
public_key = “—–BEGIN PUBLIC KEY—–\n…”
def verify_token(token):
try:
# 【重要】algorithmsをリストで固定する。
# ‘none’ は決して含めてはならない。
payload = jwt.decode(
token,
public_key,
algorithms=[“RS256″], # ここで許可するアルゴリズムを強制固定
audience=”my-app-client-id”
)
return payload
except jwt.InvalidAlgorithmError:
# 攻撃者が ‘none’ や別のアルゴリズムを送ってきたらここで弾く
logger.error(“不正なアルゴリズムが検出されました”)
raise
except jwt.ExpiredSignatureError:
logger.error(“トークンの有効期限切れ”)
raise
実装サンプル:Node.js (jsonwebtoken) での対策
Node.js環境でも同様だ。ライブラリの仕様として algorithms を指定しないと、全てのアルゴリズムを受け入れてしまう脆弱性がある。
const jwt = require(‘jsonwebtoken’);
const verifyToken = (token) => {
try {
// 署名検証時に必ずアルゴリズムを配列で明示する
const decoded = jwt.verify(token, publicKey, {
algorithms: [‘RS256’], // ここで固定しないと、攻撃者がアルゴリズムをすり替え可能
audience: ‘my-app-client-id’
});
return decoded;
} catch (err) {
console.error(‘認証失敗:’, err.message);
return null;
}
};
運用で防ぐ:さらなる防御層の構築
コードレベルの対策は必須だが、多層防御の観点から以下の設定も検討してほしい。
1. 公開鍵の配布を制限する(JWKS)
IDトークンの署名を検証するための公開鍵は、OpenID Providerの jwks_uri から取得するのが定石だ。ハードコーディングした公開鍵を使い続けるのではなく、定期的にエンドポイントを叩いて鍵をローテーションさせることで、万が一の鍵漏洩リスクを下げることができる。
2. WAFによるヘッダー監視
JWTのヘッダー部(Base64URLエンコードされた部分)を正規表現などで抽出し、"alg":"none" を含むリクエストを拒否するWAFルールを適用するのも有効だ。これはコードを修正するまでの暫定的な「盾」として機能する。
Nginx + ModSecurity のイメージ(概念)
Base64デコード後のヘッダーに none が含まれていたらブロックする
SecRule REQUEST_URI “@contains /api/v1/login” \
“id:1001,phase:2,deny,msg:’JWT none algorithm attack detected'”
最後に:エンジニアとしての矜持
「動けばいい」というコードは、数年後に必ず誰かの首を絞める。特に認証周りはシステムの心臓部だ。
今回紹介した「アルゴリズムの固定」は、セキュリティの基本中の基本だが、意外にも多くの商用アプリで見落とされている。開発チームのコードレビューを行う際は、必ず「このJWT検証、アルゴリズムを動的に受け入れていないか?」をチェックリストの最上位に置いてほしい。
セキュリティは、魔法のようなツールで解決するものではない。こうした一つひとつの泥臭い設定の積み重ねこそが、最高峰の堅牢性を生むのだ。君たちのコードが、誰かの大切なデータを守り抜くことを願っている。
コメント