【入門編】JWTのペイロードへの機密情報混入リスクと暗号化(JWE)の検討 – アプリケーションセキュリティ & 安全な開発防御ガイド

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に入れない。必要な場合はデータベースを参照する設計にする

セキュリティ対策に「絶対」はありませんが、「知っていること」は最大の防御になります。皆さんの開発するシステムが、明日も安全に動くことを祈っています!

何か疑問や「ここはどうなの?」ということがあれば、いつでも聞いてくださいね。一歩ずつ、セキュアな開発者への道を歩んでいきましょう!

コメント

タイトルとURLをコピーしました