JWTは「透明な封筒」?機密情報をうっかり入れてはいけない理由
こんにちは!セキュリティの現場で日々、システムの「お守り」をしているエンジニアです。
今日は、現代のWeb開発で避けては通れない「JWT(JSON Web Token)」についてお話しします。JWTは、ログイン状態を管理したり、サービス間でユーザーの権限を伝えたりするのに非常に便利ですよね。
でも、このJWT、実は「透明な封筒」のようなものだということを知っていましたか?
「え、暗号化されてるんじゃないの?」と思った方、大正解です。でも、その「暗号化」の正体を勘違いしていると、非常に危険な落とし穴にハマってしまうんです。今日は、JWTの仕組みと、私たちが守るべき「情報の秘密」について、一緒に紐解いていきましょう。
—
JWTの正体:署名(サイン)と暗号化は別物です
JWTの構造は、簡単に言うと「ヘッダー」「ペイロード(中身)」「署名」の3つでできています。
- ヘッダー: 「これはJWTですよ、署名にはこのアルゴリズムを使っていますよ」という説明書。
- ペイロード: ユーザーIDや権限などの具体的なデータ。
- 署名: 「このデータは改ざんされていません!」という証明印。
ここで一番の勘違いポイントは、「署名(デジタル署名)」と「暗号化」を混同することです。
泥棒の例えで考えてみましょう
あなたが友人に手紙(JWT)を送るとします。
- 署名: 封筒に「本人が書いたよ!」というサインを入れること。誰でも中身は読めますが、勝手に書き換えるとサインが一致しなくなるので「改ざん」はバレます。
- 暗号化: 鍵のかかる金庫に入れて送ること。中身を見るには、専用の鍵を持っている人しか開けられません。
JWTの標準(JWS)は、基本的には「署名」だけです。 つまり、どんなに立派なサインがあっても、封筒自体は透明なので、誰でも中身を覗き見ることができるんです。
—
なぜ「ペイロード」に機密情報を入れてはいけないのか?
JWTのペイロードは、Base64Urlという形式でエンコードされています。これは暗号化ではなく、単なる「文字の変換」に過ぎません。
例えば、ペイロードに以下のようなデータを含めてしまったらどうなるでしょうか?
{
“user_id”: “12345”,
“email”: “user@example.com”,
“role”: “admin”,
“password_hash”: “a94a8fe5ccb19ba61c4c0873d391e987982fbbd3”, // 絶対NG!
“credit_card_number”: “4111-xxxx-xxxx-1111” // 絶対NG!
}
このJWTを受け取ったクライアント(ブラウザなど)や、通信を盗み見た攻撃者は、このJSONをデコードするだけで、あなたの会社のユーザーの秘密情報を一瞬で読み取れてしまいます。これは、玄関の鍵を閉めずに、家の外に家計簿を投げ捨てているようなものです。
—
「見られてもいい情報」だけで構成するのが鉄則
では、どうすればいいのでしょうか?基本方針は「機密情報(個人情報やパスワードなど)はJWTに入れない」こと。これがセキュリティの黄金律です。
JWTに入れるべきは、あくまで「識別子(IDなど)」だけにしておきましょう。
良い例:
{
“sub”: “user_id_12345”, // ユーザーを一意に特定するIDのみ
“exp”: 1735689600 // 有効期限
}
これで、もしJWTが漏洩しても、攻撃者は「誰か」は分かっても「具体的な個人情報」まで盗み出すことはできません。
—
「どうしても情報を入れたい!」時の切り札:JWE
「いやいや、どうしてもデータを持たせないとアプリが動かないんだ!」という特別なケースもありますよね。そんな時に登場するのが JWE (JSON Web Encryption) です。
JWEは、JWTを「暗号化」する仕組みです。これを使えば、封筒そのものに鍵をかけて中身を隠すことができます。
JWEの考え方(実装のヒント)
ライブラリを使って実装する際は、以下のように「署名」だけでなく「暗号化」のステップを加えます。
// 擬似コード:JWEでの暗号化イメージ
const { CompactEncrypt } = require(‘jose’);
const payload = JSON.stringify({ ‘secret_data’: ‘見てはいけない情報’ });
// 鍵を使って暗号化する
const jwe = await new CompactEncrypt(new TextEncoder().encode(payload))
.setProtectedHeader({ alg: ‘RSA-OAEP-256’, enc: ‘A256GCM’ }) // 暗号化アルゴリズムを指定
.encrypt(publicKey); // 公開鍵で暗号化
console.log(jwe); // これなら中身は暗号化されているので安全!
ただし、JWEは処理コストがかかるため、「まずは機密情報を入れない設計ができないか?」を検討するのが先決です。
—
まとめ:セキュリティは「疑う」ことから始まる
今日覚えて帰っていただきたいのは、この2点だけです。
1. JWTは署名されているだけで、暗号化はされていない(中身は丸見え!)
2. 機密情報はJWTに入れない。必要な場合はデータベースを参照する設計にする
セキュリティ対策に「絶対」はありませんが、「知っていること」は最大の防御になります。皆さんの開発するシステムが、明日も安全に動くことを祈っています!
何か疑問や「ここはどうなの?」ということがあれば、いつでも聞いてくださいね。一歩ずつ、セキュアな開発者への道を歩んでいきましょう!
コメント