【入門編】 API認可におけるJWT(JSON Web Token)の脆弱性とベストプラクティス – 暗号理論・認証基盤 & エンドポイントセキュリティ防御ガイド

こんにちは!セキュリティの世界へようこそ。
日々、開発の現場で便利なAPIやログインの仕組みを作っていると、名前を聞かない日はないのが「JWT(JSON Web Token)」ですよね。

「ログインしたユーザーに、身分証明書の代わりに渡すデジタルパスポート」のようなものですが、このJWT、便利ゆえに扱いを間違えると、泥棒に合鍵を勝手に作られてしまうような恐ろしい落とし穴があるんです。

今回は、新人のIT担当者や「セキュリティってなんだか難しそう……」と感じている開発者の方に向けて、身近な例えを交えながら、JWTの危うい落とし穴と、絶対に守るべきベストプラクティスを一緒に紐解いていきましょう!一歩ずつ、優しく解説していくので安心してくださいね。

—

1. 家の鍵で例える「JWT」の仕組みとリスク

まずは、JWTがどんなものか、私たちの身近な「家の鍵」に例えて考えてみましょう。

あなたが賃貸マンションのオーナーだと想像してください。入居者(ユーザー)が家に入るためには、「部屋番号」と「合法的に入居者であることの証明書」が必要です。JWTは、まさにその「偽造できないデジタル証明書」の役割を果たします。

JWTの中身は、大きく分けると次の3つのパーツでできています。

1. ヘッダー(Header): 「使っている鍵の種類(アルゴリズム)」が書いてあります。
2. ペイロード(Payload): 「誰が、いつまで滞在していいか(ユーザーIDや有効期限)」というデータが入っています。
3. 署名(Signature): オーナーであるあなたが、裏面に「特製のハンコ」を押した部分です。

泥棒の手口:ハンコを勝手に偽造する!?

もし、この「特製のハンコ(署名)」が適当なものだったらどうでしょう?
泥棒がペイロードの「部屋番号」を勝手に「普通の部屋」から「管理人室(管理者権限)」書き換え、さらにハンコも自分で適当にペタッと押してマンションに入ろうとしたら……。セキュリティがガタガタになってしまいますよね。

JWTの世界でも同じことが起きます。サーバー側が「このハンコは本物か?」をしっかり確認(署名検証)しないと、攻撃者に管理者権限を奪われてしまうのです。

—

2. 攻撃者が狙う最大の盲点:alg: none 攻撃の恐怖

JWTの仕組みで、一番最初に知っておかなければいけない恐ろしい脆弱性が、alg: none 攻撃です。

先ほど、ヘッダーには「使っている鍵の種類(アルゴリズム)」が書かれていると言いました。通常は、HS256 や RS256 といった、すごく頑丈な暗号化の仕組みが指定されます。

しかし、攻撃者はこう考えました。
「なあに、ヘッダーに『署名なんていらないよ(alg: none)』って書いちゃえば、サーバーはハンコの確認をサボってくれるんじゃないか?」

攻撃のメカニズム

1. 攻撃者は自分のユーザー権限(ペイロード)を「管理者(admin)」に書き換えます。
2. ヘッダーのアルゴリズム指定を、わざと {"alg": "none"} に書き換えます。
3. サーバーにそのトークンを送りつけます。
4. もしサーバー側のプログラムが未熟だと、「おっ、ヘッダーに none って書いてあるから、今回はハンコのチェックはパスしよう!」と信じてしまい、管理者に昇格させてしまいます。

まるで、鍵のついていないドアに「鍵はかかっていません」という札を下げて、泥棒をウェルカムしてしまうような状態です。恐ろしいですよね。

—

3. 実装で防ぐ!安全なJWT検証のコード例

では、こうした攻撃からアプリを守るためにはどうすればよいでしょうか?
答えはシンプルです。「サーバー側でトークンを受け取るときに、アルゴリズムを厳密に固定し、none を絶対に許可しないこと」です。

ここでは、Node.js(jsonwebtokenライブラリ)を例に、安全な検証コードを見てみましょう。実務でもそのまま参考にしてみてください。

const jwt = require('jsonwebtoken');

// サーバーだけが知っている秘密の合言葉(実際には環境変数から安全に読み込みます)
const SECRET_KEY = process.env.JWT_SECRET_KEY;

/**
 * ユーザーから送られてきたJWTを安全に検証する関数
 * @param {string} token - クライアントから受け取ったJWT
 * @returns {object|null} - 検証成功時はデコードされたデータ、失敗時はnull
 */
function verifyUserToken(token) {
    try {
        // 【重要】algorithmsオプションに許可するアルゴリズムを明示的に指定します。
        // ここに 'none' を含めないことで、alg:none攻撃を完全にブロックできます!
        const decoded = jwt.verify(token, SECRET_KEY, { 
            algorithms: ['HS256'] // 今回は HS256 のみ許可する
        });

        console.log('トークンの検証に成功しました!', decoded);
        return decoded;

    } catch (error) {
        // 署名が不正な場合や、有効期限切れの場合はここでキャッチされます
        console.error('セキュリティ警告: 不正なJWTが検知されました。', error.message);
        return null;
    }
}

このように、algorithms: ['HS256'] と明示的に指定してあげることで、ライブラリ側が自動的に alg: none などの不正な改ざんを弾いてくれます。基本の「き」ですが、一番大切な防壁です。

—

4. 秘密鍵のローテーションと、適切なアルゴリズムの使い分け

最後に、現場のインフラやセキュリティ設計で必ず直面する「鍵の管理」についてお話しします。家の鍵も、万が一合鍵を落としたり、前に住んでいた人が鍵を持っていたりしたら、シリンダー(鍵穴ごと)交換しますよね。JWTの世界でも同じです。

共通鍵(HS256など)と公開鍵(RS256など)の使い分け

  • 共通鍵(HS256): 署名するのも検証するのも「同じ一つの秘密のパスワード」を使います。シンプルで処理が速いですが、APIサーバーが複数ある場合、すべてのサーバーに同じ秘密鍵を配る必要があるため、管理が少しリスキーになります。
  • 公開鍵・秘密鍵(RS256など): トークンを作る(署名する)ときは「秘密鍵」、検証するだけなら「公開鍵」を使います。認証サーバーだけが秘密鍵を持ち、他のAPIサーバーには公開鍵だけ配ればよいため、大規模なシステムやマイクロサービスに最適です。

秘密鍵の定期的なローテーション(更新)

どれほど頑丈な鍵でも、長年同じものを使っていれば、いつか破られるリスク(漏洩など)が高まります。そのため、定期的に鍵を新しくする「ローテーション」が必要です。

現場での運用としては、以下のようなステップを踏みます。

1. 新しい鍵(Key B)を追加する: 現在使っている古い鍵(Key A)に加え、新しい鍵(Key B)も認証サーバーに登録します。この期間は、新しい鍵で発行しつつ、古い鍵で作られたトークンもまだ有効期限内であれば検証できるようにします(複数鍵の許容)。
2. 古い鍵を廃止する: すべてのユーザーが新しいトークンに切り替わったタイミング(有効期限経過後)で、古い鍵(Key A)をシステムから削除します。

JWTのヘッダーには kid(Key ID)という、「どの鍵を使って署名したか」を示す識別子を含めることができるため、複数の鍵が混ざるローテーション期間中も、サーバー側でどの鍵を使って検証すべきかスムーズに判断できるようになっています。

—

まとめ:今日からできるセキュリティの一歩

JWTのセキュリティ対策、いかがでしたでしょうか?
難しそうに見えた攻撃も、仕組みの本質を知ればしっかりと対策を立てることができます。

  • alg: none などの不正なアルゴリズムを受け入れない(ライブラリの設定を厳格にする)
  • システムに適した暗号化アルゴリズム(HS256やRS256など)を選択する
  • 万が一に備えて、鍵のローテーションができる仕組みを意識しておく

セキュリティは一度設定して終わりではなく、日々の開発の積み重ねがそのまま強さになります。
「一歩ずつ、確実に安全なコードを書いていこう!」という気持ちを大切に、明日からの実装にぜひ役立ててくださいね。あなたの安全な開発ライフを応援しています!

コメント

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