TT Lab
はじめる
学ぶ 学習パス コース

セッションとトークン — ブラウザからメッシュまで

トークンに検証方法を指示させるな

TT Labで続きを見る

一言でいうと

JWTはトークン自身が証拠を持ち歩きます。サーバーに問い合わせる必要がなく署名を検証するだけで済む代わりに、その検証を少しでもゆるく組むと、トークン自身が「自分をこう検証しろ」と指示するとおりに検証器が従ってしまい、偽造が通ります。そのゆるさがalg混同とkid注入です。

なぜ必要なのか

JWT 1枚は、ドット2つで区切られた3つの部分、つまりヘッダー、ペイロード(クレーム)、署名です。ヘッダーにはalg(署名アルゴリズム)と、多くの場合kid(鍵の識別子)が入ります。検証器はヘッダーのalgを読んで「ああ、RS256だな」と判断し、その方式で署名を検証します。問題は、そのalgもkidもまだ検証されていない、攻撃者が好きに書いた値であるという点にあります。

ここから2つの古典的な攻撃が出てきます。1つ目として、RFC 7519 §6は、algの値が"none"の署名なしトークン(Unsecured JWS)を規格上許しています。検証器がヘッダーのalgをそのまま信じると、攻撃者はalg:noneに望みのクレームを入れ、署名の部分を空にして送ります。検証器は「noneだから検証する署名がないな」と言って通してしまいます。2つ目がalg混同です。RFC 8725の表現そのままです。「'RS256' parameters can be altered to 'HS256', leading libraries to validate using the RSA public key as an HMAC secret.」RS256は非対称なので公開鍵で検証し、公開鍵は誰でも知っています(JWKSで公開します)。攻撃者はヘッダーのalgをHS256に変え、公開鍵の文字列をHMACの秘密鍵にして自分で署名します。検証器がヘッダーのalgを信じて「HS256だからその鍵でHMAC検証」とすると、その鍵がそのまま公開鍵なので検証が通ります。公開された鍵だけで有効な署名を偽造したことになります。

どう動くのか

防御の核心は、RFC 8725 §3.1の1文です。「Libraries MUST enable the caller to specify a supported set of algorithms and MUST NOT use any other algorithms.」そして「each key MUST be used with exactly one algorithm, and this MUST be checked.」

これをコードにすると、ルールは3つです。

허용 alg 화이트리스트   검증기가 받아들일 alg 를 코드에 고정한다({RS256, HS256}).
                       none 은 목록에 없으니 자동으로 거절된다.
alg 를 키에 고정        헤더의 alg 를 믿지 않는다. kid 로 키를 찾고, 그 키의
                       타입(RSA→RS256, oct→HS256)이 정한 alg 로만 검증한다.
                       rs-1 은 RSA 키라 RS256 으로만 쓴다 — HS256 을 요구하면
                       서명을 보기도 전에 거절한다. 이것이 혼동을 막는다.
kid 는 조회 키일 뿐     kid 를 파일 경로나 URL 로 해석하지 않는다. 고정된 키
                       목록의 딕셔너리 키로만 쓰고, 없는 kid 는 거절한다.

kidをなぜここまで警戒するのか。RFC 7515 §4.1.4は「The structure of the 'kid' value is unspecified」と書いています。どんな文字列でも来られるという意味です。検証器がkidを受け取ってopen(kid)で鍵ファイルを読んだり、fetch(kid)でURLを取得したりすると、攻撃者はkidに自分の鍵ファイルのパスや自分のサーバーのURLを入れて、検証に使う鍵そのものを指定できます。RFC 8725 §3.10が、このパス注入(およびjku・x5uを通じたSSRF)を警告しています。kidは、自分が知っている鍵の中から1つを選ぶためだけの識別子でなければなりません。

署名を検証したら終わりではありません。RFC 7519のクレームを見る必要があります。exp(この時刻以降は拒否)、nbf(この時刻より前は拒否)、iss(発行者が自分の知っている相手か)、そして特にaudです。RFCは「If the principal processing the claim does not identify itself with a value in the 'aud' claim when this claim is present, then the JWT MUST be rejected」と明記しています。別のAPI向けに発行されたトークンを自分のAPIが受け付けてはいけないからです。時計のずれに備えて、数分以内の猶予(leeway)だけを置きます。

鍵が複数ある理由とkidの正当な使い方は、RFC 7517 §4.5にあります。「to choose among a set of keys within a JWK Set during key rollover.」署名鍵を新しく替えるとき、古い鍵で署名されたトークンがまだ出回っているので、JWKSに古い鍵と新しい鍵を一緒に置き、kidで選んで検証します。古い鍵を早く消しすぎると、まだ有効なトークンが一度に拒否されます。

現場での姿

alg混同は、ライブラリの1行から起きます。古いAPIがjwt.decode(token, key)のようにalgorithmsを指定せずに呼ぶと、ライブラリがヘッダーのalgをそのまま受け入れて、混同に破られます。そのため最近のライブラリは、algorithms=["RS256"]を必須の引数にしました。このラボでは、その1行がなぜ必須なのかを、検証器を手で組んで体験します。

kid注入は、ログを見ても正常な検証に見えます。「検証通過、ユーザーadmin」と出力されているのに、実は攻撃者が仕込んだ鍵で作ったトークンです。検証器がkidで何をしているかは、コードを開いて初めてわかります。

次のラボですること

cryptographyでRS256の鍵ペアとHS256の秘密鍵を作り、自分のJWT検証器を8305に立てます。自分で発行したトークンが手動検証とPyJWTの両方で通ることを確認し、その後、alg:none・混同・誤ったkidのトークンを自分で作って投げ、401で拒否されるかを確認します。JWKSで複数の鍵を置いてkidで選ぶこと、鍵のロールオーバー、そしてexp/nbf/iss/audの検証まで、一度に証明します。