JWTの検証はちゃんとするべき

に公開

はじめに

先日、あるサービスにて、面白そうなAIを無料で提供しているプロバイダを見つけました。
そこでは、ユーザーインターフェースとともにAPIが提供され、手軽にAI機能を利用できます。
認証にはJWT(JSON Web Token)が採用されており、チャットの送信や履歴の閲覧といった操作はJWTによってユーザーが検証された上で行われる設計でした。

しかし、好奇心からAPIの動作を調査していたところ、JWTのサーバーサイドにおける署名検証の不備を発見しました。これにより、IDさえ分かれば他人のチャット履歴を閲覧できてしまう脆弱性が存在したのです。

この脆弱性の発見に至った調査の経緯は、以下の通りです。

  1. アプリケーションの通信をパケットキャプチャで解析
  2. APIが提供されているサイトのフロントエンドコードを調査
  3. 公開されていたSource Mapを発見し、これを元にソースコードを詳細に解析
  4. API呼び出しにJWTが使用されていることを特定し、実際のトークンを使ってAPIを操作するWrapperを作成
  5. JWTの構造を理解するため、トークンをデコードして中身を分析
  6. テストとしてペイロード(ユーザー情報などを含む部分)を改ざんしたところ、署名が無効であるにも関わらずリクエストが成功することを確認
  7. ペイロードを空にしたり、無効な値に置き換えたりしてもリクエストが受理されてしまうことを発見
  8. APIのレートリミットが、アクセス元IPアドレスやエンドポイントごとではなく、JWT内のユーザー情報に対して設定されていることを特定
  9. この脆弱性を利用することでユーザー情報を偽装し、レートリミットを実質的に無効化できることを確認(発見後、速やかにプロバイダへ報告済み)

JWTとは?

JSON Web Tokenの略称です。以下のような、ピリオドで区切られた長い文字列で表現されます。

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.KMUFsIDTnFmyG3nMiGM6H9FNFUROf3wh7SmqJp-QV30

JWTは.によって3つの部分に区切られています。

  • ヘッダ: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
  • ペイロード: eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0
  • 署名: KMUFsIDTnFmyG3nMiGM6H9FNFUROf3wh7SmqJp-QV30

これらはそれぞれBase64UrlエンコードされたJSONデータや電子署名です。

1. ヘッダ (Header)

トークンの種類や、署名に使われるアルゴリズムの情報が含まれます。

{
 "alg": "HS256",
 "typ": "JWT"
}
キー 説明
alg HS256 署名に使われるアルゴリズム
typ JWT トークンの種類

2. ペイロード (Payload)

ユーザーIDや名前、権限など、実際に伝達したい情報(クレーム)が含まれます。

{
 "sub": "1234567890",
 "name": "John Doe",
 "iat": 1516239022
}
キー 説明
sub 1234567890 トークンの主題(ユーザーIDなど)
name John Doe 名前
iat 1516239022 トークンが発行された日時 (Unix Time)

3. 署名 (Signature)

署名は、JWTが改ざんされていないことを保証するための非常に重要な部分です。ヘッダとペイロードを連結した文字列を、ヘッダで指定されたアルゴリズム(例: HS256)とサーバーだけが知る「秘密鍵」を使って生成します。

その他、ペイロードにはsubiatのように予約されたクレーム名がありますが、詳細は他の優れた技術記事に譲ります。例えば、以下の記事が非常に分かりやすいです。
https://zenn.dev/collabostyle/articles/b08c7f29a2e94c

JWTの署名検証を怠ったときのリスク

今回のサービスでは、JWTの最大の利点である「署名による改ざん防止」が完全に機能していませんでした。この実装不備により、以下のような深刻な問題が発生しうる状態でした。

  • 認証なしでのAPIアクセス
  • ユーザー単位のレートリミットの実質的な無効化
  • ユーザーIDの総当たりによる他人の履歴の閲覧

これらは、より具体的な脅威に言い換えることができます。

  • なりすまし: 第三者が他人になりすましてサービスを不正に利用する。
  • サービスの意図的な破壊 (DoS): レートリミットを回避してAPIへ過剰なリクエストを送り、サービスを停止に追い込む。
  • 情報漏洩: 他のユーザーの個人情報や利用履歴などを不正に窃取する。

本来、サーバーは受け取ったJWTの署名を秘密鍵で検証し、トークンが改ざんされていないことを確認しなくてはなりません。
今回のケースでは、ペイロード部分にあるユーザーID(sub)を任意の値に書き換えても、署名が無効であることを見抜けませんでした。
そのため、攻撃者は他人のユーザーIDになりすましてリクエストを送信し、その人の情報を閲覧することが可能だったのです。

無効な署名を持つトークンからのリクエストは、即座に拒否するべきです。さらに、ペイロードのユーザーIDが存在しない、あるいは空であるといった不正なリクエストを許容してしまうのは、JWTの実装以前の基本的な入力チェックの問題と言えるでしょう。

まとめ

JWTはステートレスな認証を実現できる便利な技術ですが、実装を誤れば非常に高い危険性を伴います。

最低限、署名の検証は必須です。しかし、それだけでは十分ではありません。ペイロード内のデータ(例:ユーザーID)が妥当であるかの検証や、本番環境でSource Mapを公開しないといった、セキュリティに関する基本的なベストプラクティスも同時に遵守する必要があります。

このような事故を防止するために、JWTの検証は自前で実装するのではなく、信頼性の高いライブラリを利用することが強く推奨されます。

これらのライブラリを使い、適切なテストコードを実装することで、多くの脆弱性を未然に防ぐことができます。

なお、本記事で指摘した脆弱性は、報告後にプロバイダによって修正済みです。
関連する他の脆弱性についても修正が確認され次第、別途記事を用意する予定です。

あとがき

直された記念で実際に調査を行っていたり、適当にサイトからsourcemapを抜き出し復元したものをリポジトリに載せました。
聖地巡礼でもどうぞ
https://github.com/lqvp/reverse-povo-ai
https://gist.github.com/lqvp/693b70c95b10e884d0354c3330527df1
https://x.com/lvz3s/status/1962469755727777988

Discussion