こんにちは!セキュリティの勉強を始めたばかりの新人エンジニアの皆さん、日々の開発やインフラのお仕事お疲れ様です。
「JWT(JSON Web Token)」という言葉、最近のWebアプリ開発では本当によく耳にしますよね。「ログインしたユーザーが誰なのか」を証明するデジタル身分証明書のようなものですが、このJWT、便利な反面、仕組みをちょっぴり間違えると、簡単に「偽造」されてしまう恐ろしい裏の顔を持っているんです。
今回は、このJWTに潜む代表的な脆弱性――特に「署名検証のバイパス」という、ちょっとカッコよくて怖い攻撃手法について、身近な「家の鍵と合鍵」にたとえながら、分かりやすく紐解いていきたいと思います。一歩ずつ、一緒に安全なWebアプリの作り方を学んでいきましょう!
—
1. JWTってそもそも何? 身近な「合鍵付き身分証」でたとえてみよう
Webサイトにログインしたとき、サーバーは私たちに「あなたは〇〇さんですよ」という証明書を発行してくれます。これがJWTです。
例えば、近所のマンションのオートロックを思い浮かべてみてください。
管理人が発行してくれた「名前と部屋番号が書かれたカードキー」があったとします。このカードキーがあれば、エントランスを通ることができますよね。でも、もし紙切れに自分で「101号室 住民」と書いても、オートロックは開きません。なぜなら、管理人の「ホンモノの裏印(ハンコ)」が押されていないからです。
JWTもこれとまったく同じです。JWTは主に次の3つのパーツでできています。
1. Header(ヘッダー): 「何の鍵(アルゴリズム)を使ってハンコを押したか」の情報
2. Payload(ペイロード): 「誰がログインしているか(ユーザーIDや権限など)」の情報
3. Signature(シグネチャ): 改ざんされていないことを証明する「ホンモノのハンコ(署名)」
開発者の私たちが一番気をつけなければならないのは、この「3つ目のハンコ(署名)」の仕組みなんです。ここを油断すると、泥棒にハンコを偽造されてしまいます。
—
2. 攻撃者が狙う盲点:JWTの脆弱性を覗いてみよう
ペネトレーションテスト(攻撃のシミュレーション)の現場では、悪意ある攻撃者は次のような手口でJWTの安全網をすり抜けてきます。代表的な2つのパターンを見てみましょう。
攻撃パターンA:ハンコ自体を無くしてしまう「noneアルゴリズム攻撃」
JWTのヘッダー部分には、署名にどんなアルゴリズム(暗号化のルール)を使ったかが書かれています。通常は HS256(HMAC using SHA-256)などの頑丈なハンコが使われます。
しかし、古いライブラリや実装ミスがあるシステムでは、ヘッダーに {"alg": "none"} と書かれていると、サーバーがこう勘違いしてしまうことがあります。
> 「おっ、この証明書は『ハンコ不要(none)』って書いてあるな! じゃあ署名のチェックはパスしよう!」
攻撃者は、ブラウザのCookieやローカルストレージにあるJWTのペイロード部分をこっそり書き換えます(例:"role": "user" を "role": "admin" に書き換える)。そして、ヘッダーを none に書き換え、シグネチャ(ハンコ)を丸ごと削除してサーバーに送りつけるのです。
サーバーがこれを真面目にチェックしてくれなければ、一般ユーザーが瞬時に「管理者権限」を奪い取れてしまいます。これが none 攻撃の恐ろしいカラクリです。
攻撃パターンB:公開鍵を秘密鍵だと勘違いさせる「CVE-2018-0114などの鍵迷子」
少し高度な話になりますが、公開鍵暗号方式(RSAなど)を使っているシステムでも落とし穴があります。
「秘密鍵」という誰にも見せてはいけないハンコを使ってサーバーが署名し、ユーザーには「公開鍵」という検証用のハンコだけを配るのが正しい姿です。
ところが、サーバー側のプログラムが「ユーザーから渡された公開鍵を、なぜか『これがあの秘密鍵に違いない』と勘違いして信頼してしまう」という実装ミス(バリデーションの不備)があるとどうなるでしょう?
攻撃者は自分で適当なペアの鍵を作り、その「公開鍵」をサーバーに送りつけます。そして、自分が持っているその「偽の秘密鍵」で署名した偽造JWTをサーバーに送りつけると、サーバーは「おっ、俺が配った公開鍵でちゃんとチェックできるから本物だな!」と勘違いして通してしまうのです。
—
3. 実装の現場から:危ないコードと安全なコード
それでは、実際のNode.js(Expressとjsonwebtokenライブラリ)のサンプルコードを使って、何がダメで、どう直すべきかを見ていきましょう。
【NGな実装例】アルゴリズムの制限をしていない、あるいは none を許可してしまう例
const jwt = require('jsonwebtoken');
// 良くない例:受け取るアルゴリズムの指定が緩い、またはデフォルトのまま
function verifyTokenInsecure(token, secretKey) {
try {
// アルゴリズムの検証を厳密に行っていない場合、noneや意図しないアルゴリズムを通してしまうリスクがある
const decoded = jwt.verify(token, secretKey);
return decoded;
} catch (err) {
console.error("検証失敗:", err.message);
return null;
}
}
ここがダメ!
サーバー側で「どのアルゴリズムのJWTを受け入れるか」を明示的に絞り込んでいないと、攻撃者が持ち込んだ none や脆弱なアルゴリズムのトークンをスルーしてしまいます。
—
【OKな実装例】厳格なアルゴリズム指定と署名検証を行う例
const jwt = require('jsonwebtoken');
// 安全な例:想定するアルゴリズムを明示的に指定する
function verifyTokenSecure(token, secretKey) {
try {
const options = {
// 最重要:許可するアルゴリズムを配列で明示的に固定する(noneや古いアルゴリズムを排除)
algorithms: ['HS256']
};
// 厳格なオプション付きで検証を実行
const decoded = jwt.verify(token, secretKey, options);
console.log("署名検証成功! ユーザー権限:", decoded.role);
return decoded;
} catch (err) {
// 署名が改ざんされていたり、期限切れの場合はここで確実に弾かれる
console.error("セキュリティ警告:不正なトークンが検出されました。", err.message);
return null;
}
}
ここがポイント!
algorithms: ['HS256'] のように、システムが想定しているアルゴリズムをホワイトリスト形式で明示的に指定するのが鉄則です。これにより、仮に攻撃者が none や別のアルゴリズム(RS256からHS256へのすり替え攻撃など)を仕掛けてきても、ライブラリが自動的に弾いてくれます。
—
4. セキュリティを堅牢にするためのインフラ・開発チェックリスト
実務の現場でJWTを扱う際は、コードだけでなく、運用面やライブラリの選定も含めて次のポイントを必ず確認するようにしましょう。
1. ライブラリは常に最新版を使う
- JWTを処理するライブラリ(
jsonwebtoken,PyJWT,go-joseなど)には、過去に何度も深刻な脆弱性が報告されています。パッケージマネージャー(npmやpipなど)で定期的にアップデートを行いましょう。
2. 秘密鍵(Secret)は厳重に管理する
- ソースコード(GitHubなど)に秘密鍵をハードコードするのは絶対にNGです。環境変数(
.env)や、AWS Secrets Manager、HashiCorp Vaultなどの専用の秘密情報管理ツールを使いましょう。
3. 機密情報をPayloadに含めない
- JWTのペイロード(Base64URLエンコードされている部分)は、誰でも簡単にデコードして中身を読むことができます。パスワードやクレジットカード番号、個人情報などは絶対に含めず、ユーザーIDやロール(権限)などの必要最小限に留めましょう。
4. 有効期限(exp)を必ず設定する
- トークンが一度盗まれたときの被害を最小限に抑えるため、アクセストークンの有効期限は短く(例:15分〜1時間程度)し、リフレッシュトークンを併用する設計にしましょう。
—
まとめ
いかがでしたでしょうか? JWTの署名検証バイパスは、仕組みを知ってしまえば「なぜそんな単純なことで突破できてしまうのか」と驚く一方で、ちょっとした実装の油断が大きなインシデントに繋がる怖さを持っています。
- 「ハンコ(署名)のチェックは厳格に行う」
- 「受け入れるアルゴリズムは自分で絞り込む(
noneは絶対に許さない)」 - 「秘密鍵は絶対に他人に触らせない」
この基本原則を守るだけで、あなたの作るWebアプリケーションはぐっと鉄壁になります。
セキュリティの世界は奥が深いですが、一歩ずつ知識を武器にしていけば怖くありません。一緒に安全で信頼されるシステムを作っていきましょう!
コメント