CyberFix Note
セキュアコーディング

JWTの仕組みと実装の落とし穴。署名検証とアルゴリズム混同を基礎から

対象の目安: Web/APIを実装する開発者・情報システム担当 / 実務レベル

ソウ攻撃・脆弱性リサーチ担当
・ 約17分で読めます
JWTの仕組みと実装の落とし穴。署名検証とアルゴリズム混同を基礎から

JWT(JSON Web Token、ジョットと読むこともあります)は、ログイン後のセッション管理やAPIの認可で広く使われるトークンの形式です。IDとパスワードを毎回やり取りする代わりに、サーバーが発行した署名付きの小さなデータを、以後のリクエストに添えて「この利用者は認証済みで、これだけの権限を持っている」と伝えます。

便利な仕組みですが、JWTには実装で足をすくわれやすい点がいくつもあります。中でも多いのが、署名の検証をめぐる誤りです。署名を確かめないまま中身を信じてしまう、あるいは検証しているつもりでも攻撃者に検証をすり抜けられる、といった不備は、そのまま他人へのなりすましにつながります。

この記事では、Web/APIを実装する方に向けて、JWTの構造と署名の役割を一次仕様に沿って整理し、alg:noneやアルゴリズム混同(key confusion)といった典型的な落とし穴、有効期限と失効の設計、トークンの保存場所によるリスクまでを順に説明します。最初に一つ強調しておきたいのは、JWTの署名は改ざん検出のための仕組みであって、中身を隠す暗号化ではないという点です。ここを取り違えると設計が根本からずれます。

JWTの構造と3つのパート

JWTは、IETFのRFC7519で標準化されたトークンの形式です。署名して使うJWS形式のJWTでは、文字列としてドット(.)で区切られた3つのパートが並びます。この記事は、実務でよく使う署名付きのJWSを前提に説明します。中身を暗号化して使うJWE形式のJWTは5つのパートになりますが、扱いが異なるため本記事の対象外とします。

eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0Iiwibm...省略.SflKxwRJSMeKKF2QT4f...省略
   ↑ header              ↑ payload                     ↑ signature
  • ヘッダー(header): 署名アルゴリズムなど、トークンの型に関する情報を持ちます。{"alg":"HS256","typ":"JWT"} のようなJSONです。
  • ペイロード(payload): 実際に伝えたい主張(クレーム)を持ちます。利用者の識別子や発行者、有効期限などが入ります。
  • 署名(signature): ヘッダーとペイロードが改ざんされていないことを確かめるための値です。

ヘッダーとペイロードは、それぞれJSONをBase64URLで符号化したものです。Base64URLはURLで安全に扱える文字だけを使うBase64の変種で、これは暗号化ではなく単なる符号化です。つまり、元のJSONに戻すのに鍵は要りません。RFC7519も、JWTを「ドットで区切られたBase64URL符号化の各パートの並び」として定義しています。

RFC 7519「JSON Web Token (JWT)」は、JWTを一連のURLセーフなパートをドットで区切って並べたものと定義し、各パートがBase64URLで符号化された値を持つと規定しています。またペイロードに入れるクレームとして、発行者を表すiss、宛先を表すaud、有効期限を表すexpなどの登録済みクレームを定めています。

ペイロードに入るクレームのうち、RFC7519が名前と意味を定めているものを登録済みクレーム(registered claims)と呼びます。実装でよく使うものを挙げます。

クレーム意味用途の例
iss発行者(issuer)どの認証サーバーが出したトークンか
sub主体(subject)誰についてのトークンか(利用者ID)
aud宛先(audience)どのサービス/APIが受け取るべきか
exp有効期限(expiration time)この時刻以降は受理してはならない
iat発行時刻(issued at)いつ発行されたか
nbf有効開始時刻(not before)この時刻より前は受理しない

署名は改ざん検出であって、暗号化ではありません

もっとも誤解されやすい点なので、独立した節として説明します。JWTの署名は、ヘッダーとペイロードが署名鍵を持つ主体以外の手で書き換えられていないことを、受け取った側が確かめるための仕組みです。署名を検証すると、その中身が署名鍵を持つ主体によって作られ、途中で改ざんされていないことが分かります。ただし、だれが作ったと言えるかは方式によります。共通鍵のHS256では検証する側も同じ鍵を持つため、鍵を知る主体のだれかが作ったことまでしか分かりません。公開鍵のRS256では対応する秘密鍵を持つ発行者が作ったと言えますが、その公開鍵が本当に信頼できる発行者のものかを別途たしかめておく必要があります。いずれの方式でも、署名はペイロードを隠しません。

前述のとおり、ペイロードは鍵なしで元に戻せるBase64URLです。手元でデコードすれば、中に書かれた利用者IDや権限、メールアドレスなどはそのまま読めてしまいます。したがって、JWTのペイロードにパスワードやクレジットカード番号、その他の秘密情報を入れてはいけません。中身を秘匿したい場合は、署名だけのJWS形式ではなく、暗号化を伴うJWE形式を使うか、そもそも秘密情報をトークンに載せない設計にします。OWASPのJWTチートシートも、JWTは既定では署名のみで暗号化されないため、機微な情報を含めるべきではないと述べています。

注意

JWTはあくまで認可の資格情報であって、中身を隠す入れ物ではありません。「署名してあるから安全」という言い方は、改ざん検出という意味では正しくても、秘匿という意味では誤りです。ペイロードは誰でも読める前提で、載せる情報を選んでください。

署名方式のHS256とRS256の違い

署名アルゴリズムには複数の種類がありますが、実務で最初に押さえるのはHS256とRS256の違いです。

HS256は、HMACとSHA-256を組み合わせた共通鍵方式です。発行側と検証側が同じ秘密鍵(共有シークレット)を持ち、その鍵で署名と検証の両方を行います。単一のサービスがトークンを発行し、自分で検証する構成では手軽です。ただし、検証する主体すべてに同じ秘密鍵を配る必要があるため、検証者が増えるほど鍵が広がり、どこか一箇所から漏れると全員がトークンを偽造できるようになります。

RS256は、RSAとSHA-256を組み合わせた公開鍵方式です。発行側は秘密鍵で署名し、検証側は対になる公開鍵で検証します。公開鍵はその名のとおり公開してよいため、多数のサービスがトークンを検証する構成に向きます。検証側は署名を作れないので、検証者が増えても偽造のリスクは広がりません。

観点HS256(HMAC/共通鍵)RS256(RSA/公開鍵)
署名に使う鍵共有シークレット発行者の秘密鍵
検証に使う鍵同じ共有シークレット発行者の公開鍵
鍵の配布検証者全員に秘密を配る公開鍵だけ配ればよい
向いている構成発行と検証が同じ主体発行者と多数の検証者が分かれる

どちらを選ぶかは構成しだいですが、鍵の扱いを誤ると署名の意味が失われます。共有シークレットの管理は秘密情報の管理そのものであり、リポジトリへの直書きや弱い文字列は避けなければなりません。鍵や秘密情報の扱い方は関連記事で整理しています。

あわせて読みたい

シークレット管理の実務。APIキー・認証情報をハードコードせず、Vaultやマネージドサービスで守りローテーションする

ステートレスなトークン認証の考え方

従来のセッション方式では、サーバーがログイン状態をメモリやデータベースに保持し、ブラウザにはセッションIDだけを渡します。リクエストが来るたびに、サーバーはそのIDを手がかりに自分の記録を引いて状態を確かめます。サーバー側に状態を持つので、ステートフルな方式です。

JWTを使うトークン認証は、この発想を反転させます。ログイン状態や権限そのものをトークンの中に書き込み、署名で守った上で利用者に持たせます。リクエストのたびにサーバーは、渡されたトークンの署名を検証し、中のクレームを読むだけで判断できます。自己完結したトークンをサーバー側に記録を持たずに検証するこの構成が、ステートレスな方式と呼ばれます。JWTという形式そのものが常にステートレスというより、この使い方がステートレスなのだと捉えると、後の失効の話がつながります。サーバーを増やしても各サーバーが同じ検証鍵を持てばよく、水平方向に台数を増やしやすいのが利点です。

一方で、状態を持たないことは弱点にもなります。サーバーは「そのトークンをいま無効にしたい」と思っても、自分の側に記録がないため、有効期限が来るまでトークンは有効なままです。この失効の難しさは後の節で扱います。なお、JWTが扱うのは主に認可の資格情報であり、認証と認可は別の判断です。両者の違いは関連記事で整理しています。

あわせて読みたい

認証と認可の違い。Authentication と Authorization を基礎から整理する

署名検証をめぐるよくある落とし穴

JWTの事故の多くは、暗号アルゴリズムそのものの弱さではなく、検証する側の実装の甘さから起きます。代表的なものを挙げます。

署名検証の欠落や不十分

もっとも単純で、もっとも重大な誤りが、署名を検証せずにペイロードを読んでしまう実装です。JWTはデコードするだけなら鍵が要らないため、ライブラリの「デコード」関数を「検証」だと勘違いすると、署名が偽物でも中身を信じてしまいます。OWASPのテストガイドも、署名検証を伴わずにトークンを受理する実装を典型的な弱点として挙げています。検証は、ペイロードを使う前に必ず、正しい鍵で行う必要があります。

alg:none 攻撃

JWTのヘッダーには署名アルゴリズムを表すalgがあり、ここに「署名なし」を意味するnoneを指定できる仕様があります。攻撃者はトークンのヘッダーを{"alg":"none"}に書き換え、署名部分を空にして送ります。ライブラリがヘッダーのalgをそのまま信じてしまうと、「署名なし」と解釈して署名検証を省略し、改ざんされたペイロードを受理してしまいます。RFC8725は、ライブラリは呼び出し側が明示的に要求しない限りnoneのJWTを生成も消費もすべきでないと述べています。

アルゴリズム混同(key confusion)

RS256のような公開鍵方式を前提にしているのに、検証側がヘッダーのalgを信じてアルゴリズムを選んでしまうと、アルゴリズム混同という攻撃が成立します。攻撃者はヘッダーのalgをHS256に書き換え、本来は公開している検証用の公開鍵を、今度はHMACの共有シークレットとして使ってトークンに署名します。検証側が「ヘッダーがHS256だから、手元の鍵をHMACの鍵として検証しよう」と動くと、攻撃者が公開鍵のデータをHMACの共有鍵として使って生成した偽のトークンが、検証を通ってしまいます。公開鍵は誰でも入手できるため、攻撃者は正規のトークンを自由に偽造できることになります。

RFC 8725「JSON Web Token Best Current Practices」は、ライブラリが呼び出し側の指定した許容アルゴリズムの集合だけを使い、それ以外のアルゴリズムで暗号処理を行ってはならないと定めています。検証側は、受け取ったトークンのヘッダーに書かれたアルゴリズムを信頼して選ぶのではなく、自分の設定や固定した論理に基づいて検証方式を決める必要があります。

これらの落とし穴に共通する根っこは、検証すべき側が「攻撃者が自由に書き換えられるヘッダー」を信じてしまう点にあります。対策は一貫しています。検証側は、期待するアルゴリズムを自分のコードや設定で固定し、ヘッダーのalgに振り回されないことです。多くのライブラリは、検証時に許容するアルゴリズムを明示的に渡すオプションを備えています。さらにRFC8725は、アルゴリズムを固定するだけでなく、鍵ごとに使えるアルゴリズムを一つに束縛すること、そしてアクセストークンやIDトークンのように用途の異なるJWTを同じ検証処理で受けないことも求めています。

注意

JWTライブラリを使うときは、汎用の「デコード」ではなく「検証つきデコード」を呼び、許容するアルゴリズムを明示的に指定してください。ヘッダーのalgをそのまま検証方式の選択に使う実装は、alg:noneとアルゴリズム混同の両方に対して無防備になります。

有効期限(exp)と失効の難しさ

トークンは、盗まれたときの被害を有効期間の長さが左右します。expクレームは「この時刻以降は受理してはならない」という有効期限を表し、検証側はここを必ず確認します。実務では、実際にAPIを叩くためのアクセストークンは短く保ちます。どのくらい短くするかは、想定する脅威や再認証の手間、トークンを特定の送信者に結び付ける仕組みの有無で決めるもので、RFC7519もexpの具体的な長さは定めていません。短ければ、漏れても使える時間が限られます。

ただし短くすると、利用者が頻繁に再ログインを求められて不便になります。そこで、寿命の長いリフレッシュトークンを別に持たせ、アクセストークンが切れたらリフレッシュトークンで新しいアクセストークンを受け取る、という組み合わせが広く使われます。リフレッシュトークンはアクセストークンより厳重に扱い、使い回しの検知や、使うたびに置き換えるローテーションといった対策を重ねます。

厄介なのが失効(revocation)です。ステートレスなJWTは、発行してしまうと有効期限まではサーバー側の一存で無効にしにくい性質があります。利用者がログアウトしても、盗まれたトークンが判明しても、そのトークン自体はexpが来るまで検証を通り続けます。現実には、次のような補い方をします。

  • アクセストークンの寿命を短くし、失効までの窓を狭める
  • 失効させたいトークンの識別子(jti)を拒否リストに載せ、検証時に照合する
  • リフレッシュトークンはサーバー側で状態を持って管理し、無効化できるようにする

拒否リストやリフレッシュトークンの管理を入れると、その部分はステートフルになります。つまり、即時失効を求めるほど、ステートレスという利点は一部を手放すことになります。この折り合いをどこで付けるかが、トークン設計の判断どころです。

保存場所によって変わるXSSとCSRFのリスク

発行されたトークンを、ブラウザのどこに置くかも設計の一部です。置き場所によって、さらされる攻撃の種類が変わります。

localStorageはJavaScriptから読み書きできるため扱いやすい一方、HttpOnlyのような保護がなく、XSS(クロスサイトスクリプティング)でスクリプトが動くとトークンを丸ごと盗まれます。XSSが一つあれば、そのままアカウント乗っ取りにつながりかねません。

Cookieに置く場合は、HttpOnly属性を付ければJavaScriptから読めなくなり、XSSでの直接の盗み出しを防げます。Secure属性でHTTPS限定にし、SameSite属性で他サイトからの送信を制限します。ただしCookieは自動送信される性質上、CSRF(クロスサイトリクエストフォージェリ)への配慮が別途必要になり、SameSiteやCSRF対策トークンで補います。

近年よく紹介される構成は、短命のアクセストークンをメモリ上に保持し、寿命の長いリフレッシュトークンをHttpOnlyかつSecureかつSameSiteのCookieに置く、という組み合わせです。XSSとCSRFの両面に目配りした形です。ただしHttpOnlyのCookieでも、XSSが起きればスクリプトはCookieつきでAPIを呼べるため、なりすましのリクエストまでは防げません。SameSiteも万能ではなく、CSRF対策トークンやOriginの検証、そしてXSSそのものを塞ぐCSPといった対策を、置き場所に関わらず重ねる前提です。XSSの基礎は関連記事で扱っています。

あわせて読みたい

XSS(クロスサイトスクリプティング)の仕組みと対策。反射型・格納型・DOM型を原理から整理

安全に使うための実務の手順

ここまでの内容を、実装時に踏む手順としてまとめます。

JWTを安全に検証する手順

  1. 1

    トークンを受け取ったら、ペイロードを使う前に必ず署名を検証する。デコードだけで済ませない

  2. 2

    検証に使うアルゴリズムは、ヘッダーのalgではなく自分のコードや設定で固定し、許容アルゴリズムを明示的に指定する

  3. 3

    noneアルゴリズムは受理しない。ライブラリの既定が受理する場合は明示的に無効化する

  4. 4

    expで有効期限を確認し、必要に応じてnbf(有効開始)とiat(発行時刻)も確認する

  5. 5

    iss(発行者)が想定した認証サーバーか、aud(宛先)が自分のサービスかを照合する

  6. 6

    ペイロードに秘密情報を載せていないか、失効の仕組みが必要かをあわせて設計する

issaudの照合は見落とされがちですが重要です。issを確かめないと、別の発行者が出したトークンを受け入れてしまう恐れがあり、audを確かめないと、別のサービス向けに発行されたトークンを自分が受理してしまう恐れがあります。OWASPのJWTチートシートも、署名検証に加えて発行者と宛先、有効期限などのクレーム検証をあわせて行うよう勧めています。

OWASPの「JSON Web Token Cheat Sheet」は、公開鍵方式を前提にしながらアルゴリズムを確認しない実装がアルゴリズム混同攻撃を招くと指摘し、検証側は自らの設定や固定した論理に基づいて完全性を確かめ、JWTヘッダーの情報に頼って検証アルゴリズムを選んではならないとしています。あわせて、トークンの用途に応じた発行者や宛先や有効期限などのクレーム検証を推奨しています。

JWTはAPIの認可でも中心的に使われるため、個々のクレーム検証だけでなく、API全体の設計とあわせて考えると理解が深まります。

まとめ

JWT実装のチェックリスト

  • 署名は改ざん検出であって暗号化ではないと理解し、ペイロードに秘密情報を載せていない
  • ペイロードを使う前に、正しい鍵で必ず署名を検証している
  • 検証アルゴリズムを自分側で固定し、ヘッダーのalgを信じていない(alg:noneやアルゴリズム混同への対策)
  • exp、iss、audを検証し、想定した発行者と宛先のトークンだけを受理している
  • アクセストークンは短命にし、リフレッシュと失効の仕組みを設計している
  • 保存場所(localStorage/Cookie)に応じたXSSとCSRFの対策を取っている

JWTは、状態を持たずに認証と認可を運べる扱いやすい仕組みですが、その安全性は検証する側の実装にかかっています。署名は中身を隠す暗号化ではなく改ざん検出のためのものだと押さえ、ペイロードには読まれて困る情報を載せない。検証では期待するアルゴリズムを自分で固定し、expissaudまで確かめる。そして、有効期限と失効、保存場所の選択を設計として一体で考える。これらを守ることで、JWTの利点を保ちつつ、なりすましにつながる典型的な落とし穴を避けられます。

出典・参考

この記事をシェア

関連する記事