「うちのシステム、ログイン部分は前の会社が独自に作ったんですが、安全かどうか正直わかりません」——受託で保守を引き継ぐと、こうした不安をよく相談されます。認証はシステムの一番奥にあって、普段は問題なく動いているように見えます。だからこそ、トークンの検証ロジックに穴が空いていても誰も気づかないまま運用が続き、誰も中身を説明できない状態のまま引き継がれていく。動いているからといって、安全に動いているとは限りません。
きっかけは身近なところにありました。2026年初頭、軽量フレームワーク Hono の JWT/JWK 検証ミドルウェアに、署名検証のアルゴリズムが攻撃者の送るトークンヘッダーに引きずられる不具合が見つかり、修正されました。広く使われるライブラリでも、JWT 検証は油断するとこの種の穴が空きます。まして「前任者が自前で書いた」認証コードなら、なおさら点検する価値がある。本記事では、引き継いだ認証ミドルウェアに潜む典型的な脆弱性を、どう見つけ、どう安全に直すかを受託保守の立場から整理します。
JWT 検証で起きる「署名したつもり」の穴
JWT(JSON Web Token)は、ヘッダー・ペイロード・署名の三つを並べたトークンです。サーバーは受け取ったトークンの署名を検証し、「これは確かに自分が発行したもので、中身が改ざんされていない」と確認してからユーザーを認証します。逆に言えば、署名検証が甘いと、攻撃者が中身を好きに書き換えたトークンを「正規」として受け入れてしまう。これがJWT脆弱性のほとんどに共通する構図です。
最も古典的なのが alg=none 攻撃です。JWTのヘッダーには、どのアルゴリズムで署名したかを示す alg フィールドがあります。ここに none を指定すると「署名なし」という意味になり、検証ロジックがこれを素直に受け入れてしまうと、署名を空にしたまま中身を改ざんしたトークンが通ってしまいます。
# 攻撃者が作る改ざんトークン(イメージ)
ヘッダー : {"alg":"none","typ":"JWT"}
ペイロード: {"sub":"attacker","role":"admin"} ← 自分を管理者に
署名 : (空)
検証側が「alg ヘッダーの値を信じて、その方式で検証する」という作りになっていると、none を見て「署名チェック不要」と判断し、この偽トークンを通します。role を admin に書き換えるだけで管理者になりすませる、典型的な認証バイパスです(PortSwigger)。署名アルゴリズムが何であれ成立する、根の深い問題です。
「アルゴリズム混同」と Hono の事例
alg=none よりも気づきにくいのが、アルゴリズム混同(algorithm confusion)です。署名方式には大きく二系統あります。HS256 のような共通鍵方式(署名も検証も同じ秘密鍵を使う)と、RS256 のような公開鍵方式(秘密鍵で署名し、公開鍵で検証する)です。
攻撃が成立するのは、検証側が「トークンヘッダーの alg を信じて検証方式を切り替える」場合です。本来 RS256(公開鍵方式)で運用しているのに、攻撃者がヘッダーを HS256 に書き換えると、サーバーは「共通鍵方式で検証」しようとします。このとき公開されている公開鍵を共通鍵(HMACの秘密鍵)として使ってしまう実装だと、攻撃者は誰でも入手できる公開鍵で署名を捏造できてしまう。秘密鍵を知らないのに正規のトークンを偽造できる、という事故です(PortSwigger)。
冒頭で触れた Hono のケースは、この混同の系統に当たります。バージョン 4.11.4 より前の JWK/JWKS 検証では、選んだ鍵にアルゴリズムが明示されていない場合、トークンヘッダーの alg 値が署名検証に影響しうる作りになっていました。さらに、サーバーが想定している方式と、トークンの alg が一致するかをチェックしていませんでした。修正版では alg オプションの明示的な指定が必須となり、検証アルゴリズムを信用できないヘッダー値から導出しない形に改められています(CVE-2026-22817)。
教訓は明快です。検証に使うアルゴリズムは、サーバー側で固定して許可リストにする。トークンヘッダーの言い分で切り替えない。 自前実装でもライブラリ利用でも、ここが守れていなければ同じ穴が空きます。
「正しい署名」だけでは足りない — 宛先の検証
署名さえ正しければ安全か、というとそうではありません。署名が正規でも、そのトークンが「誰宛て・誰発行」なのかを確認していないと別の事故が起きます。これが iss(発行者)と aud(受信者・宛先)の検証です。
同じく Hono では、JWT 認証ミドルウェアに aud(Audience)検証の仕組みが用意されておらず、複数のサービスが同じ発行者・同じ鍵を共有している環境で、別サービス向けに発行された有効なトークンを受け入れてしまう恐れがありました(CVE-2025-62610、CVSS 8.1)。RFC 7519 は「aud クレームが存在するなら、JWTを処理する各主体は自分自身を aud の値で識別しなければならず、識別できなければそのJWTを拒否しなければならない」と定めています。バージョン 4.10.2 以降で verification.aud を指定して検証できるようになりました。
つまり JWT 検証で確認すべきは、署名の正しさだけではありません。署名・発行者(iss)・宛先(aud)・有効期限(exp)の四点を、それぞれ意図して検証する必要があります(Curity: JWT Best Practices)。引き継いだコードがこのどれかを素通りさせていないか、確かめる価値があります。
引き継ぎ時の点検手順
実際に引き継いだシステムの認証を点検するときは、次の順序で見ていきます。
- 検証アルゴリズムが固定されているか。検証関数に渡す
algorithmsが許可リストで固定されているか、それともトークンのalg任せになっていないかを確認する。noneが許可リストに混ざっていないかも見る。 issとaudを検証しているか。発行者と宛先が想定どおりかをチェックしているか。複数サービスで鍵を共有しているなら特に重要。- 有効期限とクロックスキューの扱い。
expを検証しているか、時刻ずれの許容が極端に広くないか。 - 鍵の取得経路(JWKS)の堅牢性。公開鍵を JWKS エンドポイントから取得している場合、キャッシュ戦略と鍵ローテーションへの追従が適切か。
kid(鍵ID)が見つからないときに一度だけ再取得して、見つからなければ拒否する、という挙動が安全とされています(MojoAuth)。 - 検証ライブラリのバージョン。
jsonwebtoken系・jose系・フレームワーク同梱のミドルウェアなど、使っているライブラリに既知のCVEが出ていないか、放置された古いバージョンのままでないかを確認する。
なお、ブラウザに出る「認証失敗」風のエラーが実は別レイヤーの問題、ということもあります。プリフライトの失敗を認証エラーと取り違えやすいケースはCORSエラーの正しい直し方で扱ったので、認証コードを疑う前に切り分けておくと無駄足を踏みません。
下表は、正しい検証コードが備えるべき要素の対比です。
| 検証項目 | 危ない作り | 安全な作り |
|---|---|---|
| アルゴリズム | ヘッダーの alg 任せ | サーバー側で固定・許可リスト |
| 署名鍵 | 公開鍵をHMAC秘密鍵に流用しうる | 方式ごとに鍵を厳密に分離 |
| 宛先(aud) | 未検証 | 自サービスの aud を必須化 |
| 鍵取得(JWKS) | 取得失敗時に検証を素通り | kid ミス時に再取得、なければ拒否 |
正しい検証は、おおむね次のような形に落ち着きます。
// 検証方式とaudをサーバー側で固定し、ヘッダーには委ねない
import { jwtVerify, createRemoteJWKSet } from 'jose'
const JWKS = createRemoteJWKSet(new URL('https://issuer.example/.well-known/jwks.json'))
const { payload } = await jwtVerify(token, JWKS, {
algorithms: ['RS256'], // alg=none・混同を封じる
issuer: 'https://issuer.example', // iss を検証
audience: 'service-a', // aud を検証
})
外部から取得した鍵やレスポンスを扱う処理は、fetch APIで壊れにくいHTTPクライアントを作る記事で扱ったように、HTTPクライアントの作り自体が堅牢でないと検証の前段で崩れます。認証はWebアプリ全体のセキュリティの一部でもあるので、土台を押さえたいならWebセキュリティの基本もあわせて確認すると、点検の抜けが減ります。
弊社が引き継いだ認証コードを点検した話
弊社が保守を引き継いだある会員制サービス(社名は伏せます)では、ログイン後のAPI認証が前任者の自前実装で、JWT を独自に検証していました。引き継ぎ資料には「JWTで認証している」とだけ書かれ、検証の中身は誰も説明できない状態でした。
コードを読むと、署名検証そのものは行われていたものの、検証アルゴリズムをトークンヘッダーの alg から取り出して、その方式で検証する作りでした。RS256 を前提に運用しているのに、ヘッダーを書き換えれば検証方式を切り替えられてしまう、アルゴリズム混同が成立しうる構造です。加えて aud の検証が一切なく、同じ発行基盤を使う社内の別システム向けトークンも、このAPIで通ってしまう状態でした。幸い悪用された痕跡はありませんでしたが、放置すれば「なりすましでログインできる」事故につながりかねない作りです。
直し方は、賢い検証を足すことではなく、検証の前提をサーバー側に取り戻すことでした。検証アルゴリズムをコード側で ['RS256'] に固定し、トークンヘッダーの言い分では切り替わらないようにする。iss と aud を自サービスの値で必須検証にする。そして自前の検証ロジックを、メンテナンスされている検証ライブラリの呼び出しに置き換える。差し替えの前に、既存トークンが新しい検証を通ること、改ざんトークンが弾かれることを、テストで両方確認しました。なぜこの方式に固定したのかは、後任が同じ穴を再び開けないよう記録として残しています。
この案件で効いたのは、「動いているから安全」という前提を一度外して、署名・発行者・宛先・期限を一つずつ確かめ直したことでした。認証は普段エラーを出さない分、穴が空いていても運用では気づけません。引き継いだときこそ、中身を点検する数少ない機会です。
まずどこを見るか
引き継いだシステムの認証に不安があるなら、最初に確認すべきは「検証アルゴリズムがサーバー側で固定されているか」の一点です。ここがトークンヘッダー任せになっていれば、alg=none もアルゴリズム混同も成立しうる。次に aud・iss の検証有無、そして検証ライブラリのバージョンを見れば、その認証コードがどれだけ危ういかの当たりはつきます。
引き継いだシステムの認証が自前実装で安全かわからない、JWTの検証が正しく行われているか点検したい、古い検証ライブラリを安全に置き換えたい——そうしたお悩みがあれば、グリームハブのお問い合わせからご相談ください。現行の認証コードを拝見し、署名・発行者・宛先・有効期限の検証が正しく行われているかを点検したうえで、既存の運用を止めずに安全な検証へ段階的に置き換える設計をご一緒に組みます。
Sources
- Improper Authorization in hono(CVE-2025-62610) - GitHub Security Advisory
- CVE-2026-22817: Hono JWT Alg Confusion Bypass - Miggo
- Lab: JWT authentication bypass via flawed signature verification - PortSwigger Web Security Academy
- Algorithm confusion attacks - PortSwigger Web Security Academy
- JWT Security Best Practices: Checklist for APIs - Curity
- JWKS URL and JWT Validation Guide - MojoAuth