はじめに
JWT(JSON Web Token)は、API時代の認証基盤として広く採用されていますが、その設計を誤ると 機密情報の漏えい につながる深刻なリスクを生みます。特に、JWTはクライアント側にそのまま送られ、誰でも簡単にデコードできる という性質を理解していないと、致命的な情報をトークン内に含めてしまう……という事故が実際のサービスでも多発しています。
この章では、JWT における典型的な Sensitive Information Disclosure の仕組みと、実際の誤実装例、そして正しい防御方法について整理します。
1. JWTは「丸見え」であることを忘れがち
Cookie ベースのセッションでは:
- パスワードなどの機密データは サーバ側のセッションストアに保存
- クライアントには セッションID(ランダム文字列)だけ送る
という設計が一般的です。
しかし JWT は違います。
JWT は、
- Header
- Payload(Claims)
- Signature
のうち、Header と Payload は Base64Url エンコードされているだけで、暗号化はされていない。
つまり:
JWT のペイロードは、誰でも簡単にデコード可能
(例:jwt.io / Webブラウザ / Python一行 / コマンドライン)
開発者が「サーバ側に保存しているつもり」でトークン内に敏感な情報を入れると、
その瞬間に クライアントへ公開される ということです。
2. 実際に起こる機密漏えいの例
現実のアプリケーションでよく見られる失敗例は以下の通り:
① パスワード(またはパスワードハッシュ)をJWTに入れる
→ クライアント側で丸見え
② 内部ネットワーク情報(private IP、認証サーバホスト名など)を入れる
→ 攻撃者に内部構造が漏れる
③ 重要フラグやシークレットを入れる
→ CTF 的には一撃アウト。実務でもアウト。
これらは
「JWT=セッションストレージ」
と勘違いしたときに発生します。
3. Practical Example 1(TryHackMeの演習)
まず cURL でログインリクエストを行う:
curl -H 'Content-Type: application/json' -X POST \
-d '{ "username" : "user", "password" : "password1" }' \
http://MACHINE_IP/api/v1.0/example1
すると JWT が返ってきます。
この JWT の payload 部分をデコード すると……
機密情報がそのまま入っている!
例:
{
"username": "user",
"password": "password1",
"admin": 0,
"flag": "[redacted]"
}
これは完全にアウトな実装です。
Flagをえる
curl -H 'Content-Type: application/json' -X POST -d '{ "username" : "user", "password" : "password1" }' http://10.201.43.112/api/v1.0/example1
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6InVzZXIiLCJwYXNzd29yZCI6InBhc3N3b3JkMSIsImFkbWluIjowLCJmbGFnIjoiVEhNezljYzAzOWNjLWQ4NWYtNDVkMS1hYzNiLTgxOGM4MzgzYTU2MH0ifQ.TkIH_A1zu1mu-zu6_9w_R4FUlYadkyjmXWyD5sqWd5U
このTokenをhttps://www.jwt.io/で解析して
Header
{
"typ": "JWT",
"alg": "HS256"
}
payload
{
"username": "user",
"password": "password1",
"admin": 0,
"flag": "THM{9cc039cc-d85f-45d1-ac3b-818c8383a560}"
}
4. 何が悪かったのか?(開発上の誤解)
以下のコードは、JWT を “サーバ側セッションの代わり” に使ってしまったケースです。
payload = {
"username": username,
"password": password,
"admin": 0,
"flag": "[redacted]"
}
access_token = jwt.encode(payload, self.secret, algorithm="HS256")
問題点:
- JWT の payload は「誰でも読める」
- 署名されていても「暗号化されているわけではない」
- 敏感な情報を入れてはいけない
署名は「改ざん防止」であり、「秘匿」ではない。
JWT を暗号化してくれるのは JWE(別仕様)であり、JWT(JWS)は暗号化しない。
5. 正しい修正方法
敏感な情報は JWT に入れるべきではない。
正しい設計は次のようになる:
① JWTには「識別子(username など)」だけ入れる
payload = jwt.decode(token, self.secret, algorithms="HS256")
username = payload["username"]
② パスワードやフラグなどの機密情報は「サーバ側で保持」し、必要な時にDBから取り出す
flag = self.db_lookup(username, "flag")
これにより:
- クライアントに機密情報が漏れない
- 改ざんされたデータを信じない
- JWT デコード後にサーバ側で正しい情報を取得できる
という安全なフローになる。
6. ベストプラクティスまとめ
| やってはいけない | 正しい方法 |
|---|---|
| パスワードをJWTに入れる | ✔ DBに保存し、必要時に読み出す |
| 機密データをJWTに入れる | ✔ JWTには識別子だけ入れる |
| JWTを暗号化されていると誤解する | ✔ 暗号化はしていない(署名≠暗号) |
| JWTをサーバセッションの代わりに使う | ✔ セッションはサーバ側+JWTは識別子程度 |
まとめ
JWT の payload は 誰でも読める。
ここを理解しないまま使用すると、機密情報がクライアントに漏えいするという重大なセキュリティリスクを招く。
JWT は“改ざん防止ツール”であり、“暗号化ツール”ではない。
敏感な情報を入れてはいけない。
Tools & Documents
- https://www.postman.com/
- https://swagger.io/
- https://datatracker.ietf.org/doc/html/rfc7519#section-3.1
- https://gchq.github.io/CyberChef/
- https://www.jwt.io/
- https://pyjwt.readthedocs.io/en/stable/
- https://hashcat.net/hashcat/
- https://www.openwall.com/john/