【入門編】 JWTの秘密鍵漏洩による署名偽造と権限昇格 – オフェンシブセキュリティ & リバースエンジニアリング防御ガイド

こんにちは!セキュリティの世界へようこそ。
世界中のシステムを攻撃者の視点から調査している、レッドチーム(攻撃側)エンジニアです。

今日は、Web開発で非常によく使われる「JWT(ジョット)」という技術についてお話しします。
「JWTって便利だよね」と使っている開発者の方は多いですが、実はその「鍵」の扱いを一歩間違えると、泥棒に家のマスターキーを渡してしまうのと同じくらい危険な状態になるんです。

新人IT担当者の方や、セキュリティを学び始めたばかりのエンジニアの方でも迷わないよう、身近な「家の鍵」や「身分証」に例えて、優しく丁寧に紐解いていきましょう!

—

1. JWTは「改ざん防止スタンプ付きの身分証」

まず、JWT(JSON Web Token)が何者なのかをおさらいしましょう。
JWTは、サーバーとブラウザの間で「私は誰々です」という情報をやり取りするためのチケットのようなものです。

このチケットは、大きく分けて3つのパーツでできています。

1. ヘッダー:チケットの形式(「これはJWTですよ」というラベル)
2. ペイロード:中身(「名前は太郎、権限は一般ユーザー」といった情報)
3. 署名(シグネチャ):【ここが重要!】 中身が書き換えられていないかを証明するスタンプ

泥棒と「魔法のスタンプ」の例え

イメージしてみてください。あなたは会員制の高級クラブの店番です。
お客さんは「会員証(JWT)」を持ってきます。そこには「一般会員」と書いてあります。

もし、悪賢い人が自分で自分の会員証の文字を「VIP会員」に書き換えたらどうでしょう?
そのままでは見破れませんよね。

そこで、お店側は自分だけが持っている「特別なスタンプ(秘密鍵)」を会員証に押しておきます。
もし誰かが文字を書き換えたら、そのスタンプと中身の整合性が合わなくなり、「あ、これは偽造だ!」と一瞬でバレる仕組み。これがJWTの署名の役割です。

—

2. もし「スタンプ」が盗まれたら?(署名偽造の恐怖)

ここからが本題です。
もし、お店の奥に隠してあったはずの「特別なスタンプ(秘密鍵)」が泥棒に盗まれたり、ゴミ捨て場にうっかり捨てられていたらどうなるでしょうか?

泥棒は、自分で「VIP会員」と書いた紙を用意し、盗んだスタンプをポン!と押します。
すると、店番のあなたは「本物のスタンプが押してあるから、これは本物だ!」と信じ込んで、泥棒をVIPルームに通してしまいます。

これが、「秘密鍵の漏洩による署名偽造と権限昇格」の正体です。

攻撃のステップ

1. 鍵の入手:開発者が設定ファイルやソースコード(GitHubなど)に秘密鍵を書き込んでしまい、攻撃者がそれを見つける。
2. 情報の書き換え:攻撃者は自分のJWTの中身を「一般ユーザー」から「管理者(admin)」に書き換える。
3. 署名の再作成:盗んだ秘密鍵を使って、書き換えた内容に合う「正しい署名」を自分で作り直す。
4. なりすまし:偽造したJWTをサーバーに送る。サーバーは鍵が一致するので「管理者さん、どうぞ!」とパスワードなしで通してしまう。

—

3. 【実践】コードで見る「危ない設定」と「偽造の仕組み」

では、実際にJavaScript(Node.js)の例で、どのようなコードが危険なのかを見てみましょう。

危険なサーバー側の実装例

まずは、秘密鍵がコードの中にベタ書きされている、非常に危ない例です。

const jwt = require('jsonwebtoken');

// 【絶対NG!】秘密鍵をソースコードに直接書くのは、泥棒に鍵を玄関に置いておくのと同じです
const SECRET_KEY = "my-super-secret-password-123"; 

// ログイン時にトークンを発行する処理
app.post('/login', (req, res) => {
    // 本来はDBでユーザー確認しますが、ここでは簡略化します
    const userData = {
        userId: "user_123",
        role: "guest" // 最初は「ゲスト権限」
    };

    // 秘密鍵を使って署名する
    const token = jwt.sign(userData, SECRET_KEY);
    res.json({ token });
});

// 管理者専用ページへのアクセス
app.get('/admin', (req, res) => {
    const token = req.headers['authorization'];

    try {
        // 届いたトークンを秘密鍵でチェック
        const decoded = jwt.verify(token, SECRET_KEY);
        
        // もし「role」が「admin」なら、秘密の情報を教えちゃう
        if (decoded.role === 'admin') {
            res.send("機密情報:今夜の晩御飯はカレーです。");
        } else {
            res.status(403).send("権限がありません。");
        }
    } catch (err) {
        res.status(401).send("無効なチケットです。");
    }
});

攻撃者が行う「署名偽造」のイメージ

もし攻撃者が my-super-secret-password-123 という鍵を知ってしまったら、自分のパソコンで次のようなコードを実行するだけで、管理者になりすませます。

// 攻撃者の手元での操作
const jwt = require('jsonwebtoken');
const STOLEN_KEY = "my-super-secret-password-123"; // 盗んだ鍵

// 勝手に中身を書き換える
const fakeData = {
    userId: "user_123",
    role: "admin" // ここを勝手に「admin」に変更!
};

// 盗んだ鍵で署名を作り直す
const forgedToken = jwt.sign(fakeData, STOLEN_KEY);

console.log("偽造トークン:", forgedToken);
// このトークンをサーバーに送れば、管理者に昇格できてしまいます。

—

4. 大切な家(システム)を守るための3つの防犯対策

「怖い!」と思いましたか? でも大丈夫です。正しい防犯対策を知れば、リスクはぐっと下げられます。一歩ずつ対策を学んでいきましょう!

① 鍵をソースコードに書かない(環境変数の利用)

秘密鍵はソースコード(.js や .php ファイル)には絶対に書かず、「環境変数」という、システムの深い場所に隠しておきましょう。

// .env という別ファイルに保存し、プログラムから読み込む
// SECRET_KEY=k9X#pL2mN5vR8z... (長く複雑なランダム文字列にする)
const SECRET_KEY = process.env.JWT_SECRET_KEY;

② 鍵を定期的に交換する(ローテーション戦略)

どんなに頑丈な鍵でも、万が一盗まれた時のために「定期的に鍵を交換(ローテーション)」することが重要です。
家の鍵を10年使い続けるより、1ヶ月ごとに変える方が安心ですよね。

  • 新しい鍵で新しいチケットを発行する。
  • 古い鍵もしばらくは有効にしておき、徐々に新しい鍵へ移行させる。

③ 鍵そのものを「長くて複雑」にする

「12345」のような簡単な鍵は、泥棒が適当に試しただけで突破される(総当たり攻撃)可能性があります。
人間が覚える必要はないので、コンピューターが作った「ランダムで長い文字列」を使いましょう。

—

まとめ:鍵の管理は「思いやり」

JWTの署名偽造は、技術的にはシンプルですが、その影響は「全ユーザーのデータが盗まれる」ほど甚大です。

セキュリティ対策は、最初は難しく感じるかもしれません。でも、「この鍵が誰かに見られたらどうなるかな?」と想像してみることが、立派なホワイトハッカーへの第一歩です。

1. 鍵はコードに書かず、隠す。
2. 鍵は定期的に新しくする。
3. 鍵は複雑なものを使う。

この3つを意識して、安全で楽しい開発ライフを送ってくださいね!「一歩ずつ対策を学んでいけば、必ず強いエンジニアになれますよ!」

また次回の解説でお会いしましょう!

コメント

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