【テクニカル・上級編】 JWT(JSON Web Token)のヘッダーフィールド(alg, typ, kid)の役割 – Web APIアーキテクチャ・データ連携実践ガイド

JWTヘッダーの深淵:alg・typ・kidが織りなすセキュリティの攻防と、パケットを支配する設計思想

ネットワークプロトコルやインフラの裏側を覗き見ることが好きなエンジニアであれば、私たちが普段何気なくAPIリクエストのAuthorizationヘッダーに載せているBearerトークン――その実体であるJSON Web Token (JWT)が、いかに緻密かつ危ういバランスの上で成り立っているか、興味が尽きないことだろう。

Web APIアーキテクチャにおいて、ステートレスな認証・認可のデファクトスタンダードとなったJWTだが、その構造を紐解くと、トランスポート層の暗号化(TLS)だけでは防ぎきれない、アプリケーション層固有の脆弱性が潜んでいる。特に、JWTの構造の先頭に位置する「JOSE Header(JSON Object Signing and Encryption Header)」に含まれる alg、typ、kid という3つのフィールドは、トークンの信頼性を担保する上で極めて重要な役割を果たしている。

今回は、これら3つのフィールドが持つ仕様上の意味をパケットレベルの挙動から解き明かし、世間を騒がせてきた alg: none 攻撃のメカニズムと、それを実務の現場で完全に封じ込めるための鉄壁の対策について、インフラアーキテクトの視点から徹底的に解説していこう。

—

1. JOSE Headerの解剖:alg、typ、kid の本当の役割

JWTは、Header、Payload、Signature の3つのパートがドット(.)で連結された文字列構造を持っている。このうち、最初のパートであるHeaderは、Base64URLエンコードされたJSONオブジェクトであり、後続のペイロードをどのように検証し、処理すべきかを暗号モジュールに指示する「メタデータの羅列」である。

このヘッダー内に記述される主要な3つのパラメータの仕様と、バックエンドの検証エンジンにおける挙動を見ていこう。

alg (Algorithm):署名・暗号化アルゴリズムの指定

alg は、JWTの署名(または暗号化)に使用されているアルゴリズムを明示するフィールドだ。例えば、HMAC-SHA256であれば "HS256"、RSA署名であれば "RS256"、楕円曲線暗号であれば "ES256" などが指定される。
暗号ライブラリは、この alg の値を見て、どのハッシュ関数と鍵を用いて署名を検証すべきかを動的に決定する。ここが、後述する脆弱性の温床となるポイントだ。

typ (Type):メディアタイプによるコンテキストの明示

typ は、このオブジェクトがどのような種類のトークンであるかを示す。JWTの仕様(RFC 7519)では、通常 "JWT" が指定される。
厳密な必須項目ではないが、OAuth 2.0のアクセストークンなどにおいて、JWT以外のネストされたJWT(Nested JWT)や類似のフォーマットと混同を防ぐために非常に重要な意味を持つ。一部の厳格な検証ライブラリでは、typ が意図した値であるかをチェックし、予期せぬトークン形式のインジェクションを防ぐために利用される。

kid (Key ID):鍵束(Keyring)からの公開鍵の特定

マイクロサービスアーキテクチャや分散システムにおいて、認証サーバー(IdP)が複数の秘密鍵・公開鍵をローテーションさせている場合、リソースサーバーは「どの鍵を使ってこの署名を検証すべきか」を知る必要がある。
kid は、鍵を特定するための識別子(Key ID)だ。リソースサーバーはこの kid をキーにして、JSON Web Key Set (JWKS) エンドポイントから取得したキャッシュやローカルの鍵ストアを検索し、対応する公開鍵を引き当てる。

—

2. パケットの往復と鍵解決の裏側:kid が支えるローテーション戦略

ここで、インフラストラクチャの観点から実際の認証フローにおけるパケットの動きを追ってみよう。

1. クライアントの認証: クライアントがIdPへ資格情報を送信し、JWTを受け取る。このJWTのHeaderには {"alg": "RS256", "kid": "key-2023-10-01"} のように kid が含まれている。
2. APIリクエスト: クライアントはAPI Gatewayまたはバックエンドのマイクロサービスへ、Authorization: Bearer <JWT> を付与してリクエストを投げる。
3. 鍵の引当とキャッシュ: APIサーバーのミドルウェア(JWTバリデータ)は、受け取ったJWTをパースし、Headerから kid を抽出する。

  • もしメモリ上のキャッシュに該当 kid の公開鍵が存在すれば、即座に検証フェーズに入る。
  • キャッシュになければ、IdPの /.well-known/jwks.json に非同期で問い合わせ(HTTP GET)、最新のJWKSを取得してキャッシュを更新する。

この仕組みにより、システムを停止させることなくゼロダウンタイムで鍵のローテーション(Key Rotation)が可能になる。しかし、この kid の設計を誤ると、後述するインジェクションやファイルパス走査といったセキュリティインシデントに直結する。

—

3. 恐怖の alg: none 攻撃と脆弱性のメカニズム

JWTの歴史において最も有名かつ破壊的な脆弱性の一つが、alg: none 攻撃である。

なぜ alg: none が発生するのか?

JWTの仕様(RFC 7518)では、デバッグやテストの利便性を考慮し、署名を行わないアルゴリズムとして none が定義されていた。
脆弱な実装をしているアプリケーションでは、攻撃者が次のような細工をしたJWTを送りつける。

1. Headerの書き換え: {"alg": "none", "typ": "JWT"} に変更し、Base64URLエンコードする。
2. Payloadの書き換え: 権限部分を改ざんし、"role": "admin" に書き換えてBase64URLエンコードする。
3. Signatureの削除: 末尾の署名部分を空(あるいはドットのみ)にする。

[Header (alg:none)] . [Payload (role:admin)] . [(空の署名)]

古い、あるいは設定の甘いJWT検証ライブラリ(あるいは開発者が独自に実装した検証コード)は、次のようなロジックになっている場合がある。

# 【危険な実装アンチパターン例】
import jwt


def insecure_verify_token(token):
    # 警告: algのホワイトリスト検証を行っていない!
    # 攻撃者がヘッダーの alg を "none" に書き換えると、検証がスキップされてしまう。
    header = jwt.get_unverified_header(token)
    algorithm = header.get("alg")

    if algorithm == "none":
        # 署名検証を行わずにペイロードを信頼してしまう致命的バグ
        return jwt.decode(token, options={"verify_signature": False})

    # 通常の検証フロー
    return jwt.decode(token, secret_key, algorithms=[algorithm])

このコードでは、攻撃者が alg に none を指定して送り込んだ改ざん済みトークンを、ライブラリやアプリケーションが「署名なしでOK」と誤認し、そのまま管理者権限のペイロードを受け入れてしまう。これが alg: none 攻撃の全貌だ。トランスポート層でTLSがどれほど強固であっても、アプリケーション層でこのバリデーション抜けがあれば、システムは内側から崩壊する。

—

4. 実務で実践すべき鉄壁の対策とコード実装

この脅威からシステムを守るためには、インフラおよびアプリケーションのレイヤーで明確なポリシーを強制する必要がある。

対策1: alg の厳格なホワイトリスト化(ハードコード)

ライブラリ任せにするのではなく、システムで使用を許可するアルゴリズムを明示的に指定(ホワイトリスト化)し、それ以外のアルゴリズム(特に none や、非対称暗号から対称暗号へのダウングレードを狙った HS256 へのすり替え)を完全に拒絶する。

以下に、Python(PyJWT)を用いた堅牢な検証実装のサンプルを示す。

# 【セキュアな実装例】
import jwt
from jwt.exceptions import InvalidTokenError


def secure_verify_token(token: str, public_key: str) -> dict:
    try:
        # 対策: algorithms 引数に許可するアルゴリズムを明示的に指定する。
        # これにより、ヘッダーに "alg": "none" や予期せぬアルゴリズムが指定されても例外が発生する。
        payload = jwt.decode(
            token,
            public_key,
            algorithms=["RS256"],  # 使用を許可するのは RS256 のみ
            options={
                "verify_signature": True,
                "verify_exp": True,  # 有効期限の検証を強制
                "require": ["exp", "sub", "iat"],  # 必須クレームの定義
            },
        )
        return payload

    except InvalidTokenError as e:
        # ログに詳細なエラーを出力しつつ、監査ログを記録
        print(f"Token validation failed: {str(e)}")
        raise

対策2: kid インジェクションの防止

kid をデータベースのクエリやファイルシステムのパス(ファイル名)としてそのまま使用する実装は、SQLインジェクションやパストラバーサル(Directory Traversal)の温床となる。

例えば、kid に ../../etc/passwd のような文字列を仕込まれ、サーバー内の任意のファイルを公開鍵として読み込ませる攻撃(Key Confusion / Path Traversal via kid)が存在する。

これを防ぐためには、kid の入力値を厳密にサニタイジングし、あらかじめ取得・信頼しているJWKS(JSON Web Key Set)内のキーIDリストに完全に一致するものだけを受け入れるバリデーションを実装しなければならない。

// 【Go言語による kid 検証のイメージ】
func GetSigningKey(token *jwt.Token) (interface{}, error) {
    // kid が文字列として安全か(英数字とハイフンのみか)を正規表現でチェック
    kid, ok := token.Header["kid"].(string)
    if !ok || !isValidKidFormat(kid) {
        return nil, errors.New("invalid or missing kid format")
    }

    // 信頼されたJWKSキャッシュからのみキーを検索する
    pubKey := jwksCache.Lookup(kid)
    if pubKey == nil {
        return nil, errors.New("key not found in trusted jwks")
    }

    return pubKey, nil
}

—

5. まとめ:プロトコルの隅々にまで目を光らせるエンジニアリング

JWTのヘッダー(alg、typ、kid)は、一見すると単なるJSONの断片に過ぎない。しかし、その小さな文字列が、認証・認可の境界線(Trust Boundary)を定義する極めて重要なスイッチングハブとして機能している。

ネットワークプロトコルや暗号技術を扱う私たちインフラ・セキュリティエンジニアにとって、フレームワークやライブラリのデフォルト設定をうのみにせず、「パケットレベルで何が行われているか」「悪意ある改ざん者がどのようにこの仕様の隙を突いてくるか」を常に想像し続ける姿勢が不可欠だ。

APIの設計やレビューを行う際は、今一度、認証ミドルウェアの alg ホワイトリスト設定と kid のハンドリング処理がセキュアに実装されているか、コードの深部まで目を光らせてほしい。

コメント

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