JWT(JSON Web Token・CWE-347: 署名検証の不備が典型)は、サーバがログイン済みの事実を、署名付きの小さなトークンに入れて持ち歩かせる仕組みです。便利な一方で、検証の仕方を一つ間違えると、署名をすり抜けて「誰にでもなりすませる」入口になります。怖いのは、見た目は正しく動き、テストも通ってしまうことです。落とし穴の多くは「トークンに書いてあることを、サーバが信用しすぎる」ことから生まれます。

本記事は連載「事故から逆算するWebセキュリティ」の第29回です。JWT を発行・検証するコードを書く開発者に向けて、何が危険で、どう検証するかを整理します。セキュリティ全体の地図は「サプライチェーン攻撃とは|手口・防御ツール比較・npm 12の新機構まで実践解説」にまとめています。

HEADER vs SERVER根っこは『誰がアルゴリズムを決めるか』

トークンのヘッダを信じると危険/サーバ側で固定して検証すれば安全

危ない:トークン任せ

  • ヘッダの alg をそのまま使って検証
  • alg: none を受理(署名を検証しない)
  • RS256 のつもりが HS256 にすり替えられる
  • 署名は見ても exp・aud・iss を見ない

安全:サーバが決める

  • 受け付ける alg をサーバ側で固定(許可リスト)
  • none を拒否し、署名を必ず検証
  • 鍵は用途を分ける(HS の鍵で RS を検証しない)
  • exp・nbf・iss・aud まで必ず検証
出典: RFC 7519 / OWASP JWT Cheat Sheet / CWE-347 の方針(2026-11-06 整理)

30秒でわかる

  • ・何が起きるか:JWT の検証を誤ると、署名をすり抜けて任意のユーザーになりすまされる(CWE-347)
  • ・典型の落とし穴:alg: none の受理、RS256→HS256 のアルゴリズム混同、署名を検証せず decode するだけ、exp/aud/iss 未検証
  • ・なぜ危険か:多くのライブラリがトークンのヘッダに書かれた alg を信じて検証方法を決める
  • ・効く対策:受け付ける alg をサーバ側で固定し、署名を必ず検証し、クレーム(exp・iss・aud)まで検証する

JWT はしばしばセッション管理とCookieの属性の代わりに使われますが、両者は性質が違います。サーバ側に状態を持つセッションは失効させやすく、JWT は持ち歩けるぶん失効が難しい。多要素の話は多要素認証とパスキーと合わせて設計します。

JWT はどこで使われ、どこで間違うのか

影響を受けるのは、JWT を検証して認証・認可に使うすべての処理です。ログイン後の API 認証、サービス間のトークン、OAuth/OIDC の ID トークンなどが該当します。JWT は3つの部分(ヘッダ・payload・署名)を . でつないだ文字列で、ヘッダと payload はBase64URL エンコードされているだけ——暗号化ではありません。署名が守るのは「改ざんされていないこと」だけで、「読まれないこと」ではない点が、最初の誤解のもとになります。

JWT が選ばれる理由は、たいてい「サーバ側に状態を持たなくてよい(ステートレス)」ことです。トークンの中に本人や権限が入っていて、署名さえ正しければサーバは記録を引かずに信頼できる——という身軽さが魅力です。ところが、その身軽さは失効の難しさと裏表です。サーバが何も覚えていないということは、「このトークンはもう無効だ」と後から言う場所も無い、ということ。だから「ログアウトしたのにトークンはまだ有効」「権限を剥奪したのに古いトークンで入れる」という事故が起きます。ステートレスの利点を取るなら、失効をどう設計するかまでを一緒に決めておく必要があります。これが、後述する「短命+リフレッシュ」や「重要操作だけサーバ側で確認」という折衷につながります。

JWT のよくある落とし穴

COMMON PITFALLSよくある落とし穴

検証の抜けと、ヘッダの信用

  1. 1.alg: none / 署名の未検証

    alg に none を指定されると署名なしで通る。decode だけして verify しない実装も同じ(中身は base64 で誰でも読める・書ける)

  2. 2.アルゴリズム混同(RS256→HS256)

    公開鍵で検証する前提なのに、その公開鍵を HMAC の鍵として HS256 で署名され、通ってしまう

  3. 3.クレーム未検証

    exp(期限)・nbf・iss(発行者)・aud(宛先)を見ず、期限切れや別用途のトークンを受理する

  4. 4.payload に秘密を入れる

    JWT の payload は暗号化ではなく base64。秘密情報を入れると誰でも読める

  5. 5.失効できない

    JWT は exp まで有効。ログアウトや漏洩時に即時無効化する手段を用意していない

  6. 6.kid / jku / jwk をそのまま信頼

    ヘッダの鍵ID・鍵URLを検証せず使うと、パストラバーサルや攻撃者の鍵での検証に繋がる

出典: OWASP JWT Cheat Sheet が挙げる典型を整理(2026-11-06)。悪用の具体手順は非掲載

・alg: none / 署名の未検証:ヘッダの alg に none を指定されると、署名なしで通ってしまう実装がある。また、トークンを decode する(中身を取り出す)だけで verify(署名検証)しない実装も同じで、payload は誰でも書き換えられる
・アルゴリズム混同(RS256→HS256):公開鍵で検証する前提(RS256)なのに、ライブラリがヘッダの alg を信じるため、攻撃者が公開鍵を HMAC の鍵として HS256 で署名したトークンを通せてしまう。公開鍵は公開情報なので、これが成立すると致命的
・クレーム未検証:exp(期限)・nbf・iss(発行者)・aud(宛先)を見ず、期限切れトークンや別サービス向けのトークンを受理する
・payload に秘密を入れる:Base64 なので誰でも読める。パスワードや鍵、個人情報を入れてはいけない
・失効できない:素の JWT は exp まで有効で、ログアウトや漏洩時に即時無効化する手段がない
・kid / jku / jwk をそのまま信頼:ヘッダの鍵ID(kid)や鍵URL(jku)を検証せず使うと、パストラバーサルや、攻撃者が用意した鍵での検証に繋がる

共通するのは、「トークンのヘッダ(attacker が自由に作れる部分)を信用している」ことです。攻撃者が送れる入力と、守りが無いときに起こることを整理します。

攻撃者が送れる入力 守りが無いと起こること
alg: none のトークン 署名なしで検証を通過する
公開鍵で HS256 署名したトークン RS256 前提のサーバがアルゴリズム混同で受理する
期限切れ・別 aud のトークン クレーム未検証なら有効なものとして扱われる
kid に細工した値 鍵の取り違え・パストラバーサルに繋がる

JWTの落とし穴対策——algを固定し、署名とクレームを検証する

効くのは、検証の主導権をトークンからサーバに取り戻すことです。OWASP の JWT Cheat Sheet も、受け付けるアルゴリズムを明示的に指定し、none を拒否し、クレームを検証することを勧めています。順序はこうです。

SAFE DESIGN設計の順序

alg固定 → 署名必須 → クレーム検証 → 失効の設計

  1. 1

    アルゴリズムをサーバで固定

    受け付ける alg を1つ(か許可リスト)に固定。トークンの alg ヘッダを信用しない。none は常に拒否

  2. 2

    署名を必ず検証

    decode と verify を混同しない。検証に成功したトークンだけ信頼する。鍵は用途ごとに分ける

  3. 3

    クレームを検証

    exp・nbf・iss・aud を必ずチェック。宛先(aud)が自分、発行者(iss)が想定どおりかまで見る

  4. 4

    失効と最小化を設計

    アクセストークンは短命+リフレッシュ。重要操作はサーバ側のセッション/denylist で失効可能に。payload に秘密を入れない

出典: OWASP JWT Cheat Sheet / RFC 7519 の方針(2026-11-06)。鍵管理と失効は運用側の設計

ポイントは、受け付けるアルゴリズムをサーバ側で固定することです。検証関数に「許可する alg」を明示的に渡し、トークンのヘッダの alg を判断材料にしない。これで none もアルゴリズム混同も塞げます。守り方ごとに、何を防ぎ、何を防がないかを並べます。

守り方 何を防ぐか 何を防がないか
ヘッダの alg を信じて検証 (防がない。混同・none の温床) alg: none・RS256→HS256 混同
受け付ける alg をサーバで固定 none・アルゴリズム混同 クレームの検証漏れ・鍵の漏洩
+ exp・iss・aud を検証 期限切れ・別用途トークンの流用 失効(exp まで有効)・payload の露出
短命+リフレッシュ/denylist 漏洩時の有効期間・即時失効 設計・運用の複雑さ(別途管理)

アルゴリズム混同がなぜ成立するのかも、一度押さえておくと対策が腑に落ちます。RS256 は「秘密鍵で署名し、公開鍵で検証する」非対称方式、HS256 は「同じ秘密鍵で署名も検証もする」共通鍵方式です。サーバが「ヘッダの alg を見て検証方法を選ぶ」実装だと、攻撃者は alg を HS256 に書き換え、誰でも入手できる公開鍵を『共通鍵』として署名したトークンを送れます。サーバは手元の公開鍵で HMAC 検証してしまい、署名が一致する——これが混同攻撃の正体です。だから「alg をサーバ側で固定する」ことが、単なる作法ではなく核心になります。

もう一つ、HMAC(HS 系)の秘密鍵の強度も見落とされがちです。短く推測しやすい秘密を使うと、流出していなくてもオフラインで総当たりされ、正規のトークンを偽造されます。HS 系なら十分に長くランダムな鍵を使い、できれば用途ごとに分け、リポジトリや payload に置かないこと。鍵の置き場所の考え方は連載の鍵管理の回にも通じます。

加えて、署名の検証(verify)とデコード(decode)を混同しないこと。ライブラリによっては「中身を取り出すだけ」の関数があり、それを認証に使うと署名を素通りします。鍵は用途ごとに分け(HS の鍵で RS を検証しない)、exp・iss・aud を必ず検証し、payload に秘密を入れないこと。自前で検証を実装するより、保守されているライブラリの検証関数に「許可する alg」と「期待する iss・aud」を明示的に渡す形が、取りこぼしを減らせます。そして素の JWT は失効できないので、アクセストークンは短命にしてリフレッシュで更新し、重要操作はサーバ側で失効を確認できるように設計します。

鍵の取り違えも塞いでおきます。ヘッダの kid(鍵ID)でサーバ側の鍵を選ぶ設計は一般的ですが、kid の値をそのままファイルパスやクエリに渡すと、パストラバーサルや SQL インジェクションの入口になります。kid はあらかじめ登録した鍵の集合への索引として扱い、知らない値は拒否します。さらに jku(鍵を取りに行く URL)や jwk(トークンに埋め込まれた鍵)をそのまま信用して鍵を取得しないこと。これらを信じると、攻撃者が用意した鍵で「正しい署名」を作れてしまいます。鍵の取得先は、トークンではなくサーバ側の設定で固定するのが原則です。

自分のコードを確認する

ここからは読み取りだけの確認コマンドです。いずれも判定ではなく構成の確認で、ヒットの有無だけでは安全とは言えません。該当箇所を目で追う起点として使います。. は自分のリポジトリのルートに読み替えてください。

まず、JWT の検証・デコードを呼んでいる箇所を洗い出します。

grep -rniE "(jwt\.(decode|verify)|jsonwebtoken|jose|jwt_decode|verify_jwt|decodeJwt)" . --include="*.js" --include="*.ts" --include="*.py" --include="*.go" --include="*.rb" --include="*.java" --include="*.php"

次に、危険な兆候(alg の無指定・none 許可・検証なしの decode・クレーム未検証)が無いかを確かめます。

grep -rniE "(alg.*none|algorithms?\s*[:=].*\[\s*\]|verify\s*[:=]\s*false|decode[^A-Za-z].*verify\s*[:=]\s*false|noVerify)" . --include="*.js" --include="*.ts" --include="*.py" --include="*.go" --include="*.rb" --include="*.php"

いずれも、小さなダミーのプロジェクトで陽性(alg をトークン任せ・decode のみにした側)と陰性(alg を固定し verify+クレーム検証に直した側)の両方を走らせ、拾うべきものを拾い、無いときに何も出ないことを確かめてから載せています。ヒットした箇所が本当に危険かは、受け付ける alg がサーバ側で固定されているか、署名検証とクレーム検証が後ろにあるかを、コードを読んで確かめます。悪用に使えるトークンの具体的な細工は、本記事では扱いません。

参照ソース