【実務・中級編】JWTのクレーム検証におけるiss, aud, expの厳密なチェック実装 – アプリケーションセキュリティ & 安全な開発防御ガイド

JWTの「検証漏れ」は、泥棒に合鍵を渡すのと同じだ:iss, aud, expを軽視するな

「JWT(JSON Web Token)を使っています。署名検証はライブラリに任せているので大丈夫ですよね?」

セキュリティレビューの現場で最も耳を塞ぎたくなる言葉の一つだ。多くのエンジニアは「署名の検証(Verify Signature)」さえしていれば安全だと誤解しているが、それは大きな罠だ。署名が正しくても、クレーム(Payload内の属性)の検証が不十分であれば、システムは無防備な状態に等しい。

今日は、JWTの「iss」「aud」「exp」を放置することで、攻撃者にどのような特等席を用意してしまうのか、そしてどうやってそれを防ぐのか、現場のリアルな視点から解説する。

—

1. なぜ「iss」「aud」「exp」の検証が必須なのか?

JWTは「自己完結型」のトークンだが、それは「検証も自前で完結させろ」という意味でもある。

  • iss (Issuer / 発行者): どこの認可サーバーが発行したか。これが未検証だと、攻撃者が自分で立てた偽の認可サーバーで発行したトークンを、君のシステムが受け入れてしまう。
  • aud (Audience / 対象者): このトークンは「君のシステム」に向けられたものか。これが未検証だと、Aというシステム向けに発行されたトークンを、君のシステム(B)で使い回す「リプレイ攻撃」が成立する。
  • exp (Expiration Time / 有効期限): 期限切れかどうか。これが未検証だと、一度盗まれたトークンは、攻撃者がパスワードを変更しようが何しようが、永遠に有効な「魔法の合鍵」として君臨し続ける。

攻撃者の視点:
君のアプリが exp を見ていなければ、攻撃者は数ヶ月前に盗んだトークンを捨てずに保管し、深夜の監視が手薄な時間帯にバックドアを仕込む。aud を見ていなければ、脆弱な別アプリのJWTを流用して、君の管理画面にログインを試みる。これらは古典的だが、今なおインシデントのトップランナーだ。

—

2. セキュアな実装サンプル(Node.js / jsonwebtoken)

ライブラリを「デフォルトのまま」使うのが一番危ない。検証オプションを明示的に指定し、ホワイトリスト運用を行うのがプロの流儀だ。

const jwt = require(‘jsonwebtoken’);

// 厳密な検証オプションを定義する
const verifyOptions = {
issuer: ‘https://auth.example.com’, // 信頼できる発行元を固定
audience: ‘https://api.my-service.com’, // 自システムの識別子
algorithms: [‘RS256’], // HS256(対称鍵)は避け、必ずRS256(非対称鍵)を使う
clockTolerance: 30 // 時計のズレを考慮しつつも、過度な猶予は与えない
};

try {
// 公開鍵を使って検証
const decoded = jwt.verify(token, publicKey, verifyOptions);

// さらに、アプリ固有のロジックで「ユーザー状態」を再確認する
// トークンが正しくても、DB上でアカウントが凍結されていないか?
if (await isUserBlocked(decoded.sub)) {
throw new Error(‘User account is suspended’);
}

console.log(‘検証成功:’, decoded);
} catch (err) {
// ここでログを出す際は、機密情報を含めないよう注意
console.error(‘JWT検証失敗: 不正なトークンまたは期限切れ’);
}

—

3. インフラレイヤーでの「防御の多層化」

コードレベルでの検証に加え、インフラ側でもJWTを保護する姿勢を持とう。特に、API GatewayやNginxレベルでの簡易チェックは、アプリケーションまで攻撃を通さないための「防波堤」になる。

Nginxでの検証設定(Luaモジュール使用例)

アプリケーションの手前にあるNginxで exp のチェックを代行させることで、期限切れトークンをアプリケーションまで到達させない設計にする。

Nginx + lua-resty-jwt の例
location /api/ {
access_by_lua_block {
local jwt = require “resty.jwt”
local jwt_obj = jwt:verify(“MY_SECRET_KEY”, ngx.var.http_authorization)

— 有効期限の強制チェック
if not jwt_obj.verified then
ngx.exit(ngx.HTTP_UNAUTHORIZED)
end
}
proxy_pass http://backend_upstream;
}

—

4. プロの現場で守るべき3つの鉄則

1. HS256(対称鍵)は使うな: 署名鍵と検証鍵が同じだと、万が一ソースコードが流出した際に全トークンが偽造可能になる。必ずRS256やES256のような「非対称鍵」運用を行うこと。
2. exp は短く、かつ更新可能に: トークンの有効期間は15分〜1時間程度に留め、リフレッシュトークン(RefreshToken)を用いたセッション更新フローを実装せよ。
3. エラーメッセージを詳細にするな: クライアントへのレスポンスで「Issuerが違います」や「Audienceが不正です」と返してはならない。攻撃者に「どこまで正解に近づいているか」のヒントを与えることになる。常に「Invalid Token」とだけ返し、詳細な理由はサーバーのログにのみ残すこと。

まとめ

JWTの検証は「ライブラリに任せた」時点で、セキュリティの放棄が始まる。iss で発行元を縛り、aud で目的を限定し、exp で寿命を管理する。この3つを徹底するだけで、君のアプリの強度は劇的に向上する。

泥臭いかもしれないが、こうした「当たり前の検証」を徹底できるエンジニアこそが、最後にインシデントの火種を消し止めるのだ。さあ、今すぐプロジェクトのソースコードを開いて、検証オプションが空っぽになっていないか確認してくれ。

コメント

タイトルとURLをコピーしました