当社のAIエンジンNeutronが、UCバークレーのCyberGymベンチマークで97.1%のスコアを記録しました。 詳細を見る

ディープダイブ

ディープダイブ

Authlib 1.9.0で修正されたJWS署名バイパス(CVE-2026-96760)

Authlib 1.8.0以前は、signaturesが空のJWS JSONオブジェクトを、鍵を確認せずに検証済みとして扱います。重大度クリティカルのCVE-2026-96760(CVSS 9.1)で、1.9.0で修正されています。

TL;DR

  • PythonのOAuth、OpenID Connect、JWT/JWSライブラリであるAuthlibは、signatures配列が空のJWS JSONシリアライゼーションオブジェクトを受け入れ、署名を一つも検査せず、呼び出し元が渡した検証鍵を一度も参照しないまま、そのペイロードを検証成功として返していました。この欠陥はCVE-2026-96760で、authlib/jose/rfc7515/jws.pyのJsonWebSignature.deserialize_json()に存在する、重大度クリティカルの署名検証バイパスです。
  • ベンダーのアドバイザリGHSA-6v38-4h7w-rj9hによると、1.8.0までのすべてのリリースが影響を受けます。修正は2026年10月14日にリリースされたAuthlib 1.9.0に含まれています。
  • ベンダーのアドバイザリはCVSS v3.1で9.1(クリティカル)と評価し、CVEレコードのCISA ADPエンリッチメントは9.8(クリティカル)と評価しています。2つのスコアの違いは可用性への影響のみです。
  • CISAのSSVC評価は2026年10月1日時点でExploitation: noneと記録しており、この欠陥は2026年10月27日時点でCISA KEVカタログに掲載されていません。公開の概念実証(PoC)がアドバイザリに掲載されています。
  • Authlib 1.9.0以降にアップグレードしてください。それまでは、signaturesメンバーが存在するものの空であるJWS JSON入力をAuthlibに渡す前に拒否するか、影響を受けないコンパクトシリアライゼーションのトークンのみを受け入れてください。
  • AuthlibのPyPIダウンロード数は、直近1週間で28,498,200、直近1か月で107,757,780でした(pypistats、2026年10月27日確認)。

影響を受けるか

以下のすべてに該当する場合、影響を受けます。

  • Pythonサービスがauthlibパッケージの1.8.0以前をインストールしており、そのjoseモジュールを使ってJSON Web SignatureまたはJSON Web Tokenを検証している
  • サービスが、ドット区切りのコンパクトトークンに加えて、またはその代わりに、RFC 7515 section 7.2.1で定義された{"payload": ..., "signatures": [...]}オブジェクトである一般JWS JSONシリアライゼーションを受け入れている
  • JSONシリアライズされたトークンがJsonWebSignature.deserialize_json()、またはdict入力とJSONテキストをdeserialize_json()に転送するJsonWebSignature.deserialize()に到達する
  • トークンの入手元が攻撃者から到達可能である(リクエストボディ、Webhook、メッセージキュー、公開APIのヘッダーなど)

以下の場合は影響を受けません。

  • コンパクトシリアライゼーションのトークン(header.payload.signature)のみを検証している場合:deserialize_compact()は無条件に署名を検証します。
  • フラット化JSON形式({"payload": ..., "protected": ..., "signature": "..."})のみを受け入れている場合:この形式は別の分岐を通り、単一の署名を検証します。
  • JsonWebToken.decode()、つまりほとんどのJWTコードが使うjwt.decode()呼び出しでトークンを検証している場合:入力のドットの数を数え、ドットが2つのトークンをdeserialize_compact()に(ドットが4つのトークンはJWEに)送り、それ以外にはDecodeErrorを送出するため、deserialize_json()には到達しません。1.8.0(authlib/jose/rfc7519/jwt.py)では、偽造された{"payload": ..., "signatures": []}オブジェクトはドットを含まないためここで拒否されますが、JsonWebSignature().deserialize()はこれを受け入れます。
パッケージ 影響を受けるバージョン 修正バージョン
authlib(PyPI) 1.8.0までのすべてのリリース 1.9.0

インストールされているバージョンを確認します。

pip show authlib | grep -E "^(Name|Version)"

ロックファイルで固定されているバージョンを確認します。

grep -iE '^authlib' requirements.txt requirements/*.txt 2>/dev/null
grep '"authlib"' Pipfile.lock 2>/dev/null
grep -A2 'name = "authlib"' uv.lock 2>/dev/null | grep -E 'name|version'
grep -iE 'authlib' pyproject.toml poetry.lock 2>/dev/null

コードが影響を受けるエントリーポイントを使っているか確認します。

grep -rn "deserialize_json\|deserialize(" --include="*.py" . | grep -v test

コンテナの場合は、ビルドしたつもりのイメージではなく、実行中のコンテナを確認します。

docker exec <container> pip show authlib | grep Version

修正方法

Authlibを1.9.0以降にアップグレードし、デプロイ済みのコンテナやサーバーイメージを再作成してください。そうすることで、修正が最新のイメージタグだけでなく、実際に動作しているプロセスに反映されます。

pip install --upgrade "authlib>=1.9.0"

まだアップグレードできない場合は、入力がAuthlibに届く前にフィルタリングします。signaturesメンバーが空リストであるJWS JSONオブジェクトはすべて拒否してください。これがCVE-2026-96760の経路です。このフィルターはnullのメンバーも拒否しますが、AuthlibはこれをすでにTypeErrorで拒否しており、ここで拒否してもそのクラッシュが整ったBadSignatureErrorに変わるだけです。このメンバーを持たないオブジェクトは通してください。それはフラット化形式であり、Authlibがすでに検証しています。そのうえで、すべてのdeserialize()およびdeserialize_json()呼び出しを、トークンをdeserialize()に渡す前にフィルターを適用する一つのラッパー経由にします。

import json
from authlib.jose import JsonWebSignature
from authlib.jose.errors import BadSignatureError

_jws = JsonWebSignature()

def _reject_empty_signatures(obj):
    if "signatures" in obj and not obj["signatures"]:
        raise BadSignatureError("JWS carries no signatures")

def safe_deserialize(data, key, decode=None):
    if isinstance(data, dict):
        _reject_empty_signatures(data)
    else:
        raw = data.encode() if isinstance(data, str) else data
        if raw.startswith(b"{") and raw.endswith(b"}"):
            _reject_empty_signatures(json.loads(raw))
    return _jws.deserialize(data, key, decode)

このラッパーはdeserialize()が形式を選ぶ方法をなぞっています。dictとJSONテキストはフィルターを通り、コンパクトトークンはそのままAuthlibに渡されて検証されます。この回避策には限界があります。ラップした呼び出し箇所しか保護しないため、一般シリアライゼーションのJWSを受け入れるすべての経路に同じチェックが必要です。また、Authlib自体は変わらないため、今後ラッパーを経由しない呼び出しは保護されません。この回避策が対象とするのは空のsignaturesの経路であるCVE-2026-96760です。コンパクトおよびフラット化の入力は、Authlib内部ですでにフェイルクローズします。

根本原因

authlib/jose/rfc7515/jws.pyのJsonWebSignature.deserialize_json()は、何も確認しないうちに有効性フラグをTrueで初期化し、Falseに設定するのはトークンのsignatures配列を回すループの内側だけです。そのため、signaturesリストが空の一般シリアライゼーションJWSはすべての検証ステップを飛ばし、呼び出し元が有効な鍵を渡していても、検証成功として返されます。

v1.8.0のソース(204行目以降)から読み取った脆弱なコードは次のとおりです。

headers = []
is_valid = True
for header_obj in obj["signatures"]:
    jws_header, valid = self._validate_json_jws(
        payload_segment, payload, header_obj, key
    )
    headers.append(jws_header)
    if not valid:
        is_valid = False

rv = JWSObject(headers, payload, "json")
if is_valid:
    return rv
raise BadSignatureError(rv)

これをクラッシュではなく検証バイパスにしている要因は3つあります。

  • is_validはTrueから始まります。その初期状態は、署名を一つも検査する前からオブジェクトが有効であると主張しています。
  • is_valid = Falseを設定できる唯一の場所であるループ本体は、obj["signatures"]のエントリーごとに1回実行されます。空リストでは0回しか反復しないため、フラグは初期値のTrueのままです。
  • 呼び出し元のkeyは_validate_json_jws()の内側でのみ使われ、空のループはこれを一度も呼び出しません。アプリケーションが注意深く渡した検証鍵は、まったく参照されません。

このメソッドの契約上、返されたオブジェクトは信頼できるものとして扱われます。deserialize_json()はBadSignatureErrorを送出することでしか拒否を示さないため、呼び出しが例外を送出せずにJWSObjectを返すと、呼び出し元はそのペイロードを検証済みとして扱います。エントリーポイントが影響範囲を広げます。deserialize()はdict入力と、{で始まり}で終わるあらゆるJSONテキストをdeserialize_json()に転送し、コンパクトトークンは常にalgorithm.verify()を実行するdeserialize_compact()に送られます。

悪用方法

攻撃者は、signatures配列が空の一般JWS JSONシリアライゼーションでペイロードを包んで送信します。呼び出し元の鍵が一度も参照されず、オブジェクトのどこにも署名が存在しないにもかかわらず、アプリケーションはそれを検証済みとして返します。

アドバイザリの公開PoCがこれを実証しています。最後の行は本記事で追加したもので、アドバイザリ自身の実行記録にある鍵を渡したケースにならい、本物の鍵を渡しています。

import base64
from authlib.jose import JsonWebSignature

def b64(b):
    return base64.urlsafe_b64encode(b).rstrip(b"=").decode()

jws = JsonWebSignature()
payload = b'{"sub":"attacker","role":"admin"}'
forged = {"payload": b64(payload), "signatures": []}

print(jws.deserialize_json(forged, key=None))
print(jws.deserialize_json(forged, key="the-apps-real-hs256-secret"))  # still accepted

アドバイザリは、Python 3.14上にインストールしたAuthlib 1.8.0の成果物で観測された結果を記録しています。偽造オブジェクトは、key=Noneでも本物の鍵でも同じようにACCEPTEDとなります。

次の図は、偽造ペイロードがアプリケーションに信頼されるまでの流れを示しています。

flowchart LR A["攻撃者がクレームを作成
{sub, role, scopes}"] --> B["一般JSONシリアライゼーション内の
Base64urlペイロード"] B --> C["signatures: []
署名は存在しない"] C --> D["deserialize_json() / deserialize()
アプリの鍵を指定"] D --> E["検証チェックは実行されない:
signaturesのループは
0回しか反復しない"] E --> F["is_validはTrueのまま、
ペイロードは検証済みとして返される"] F --> G["偽造されたID、ロール、
認可が受け入れられる"] style E stroke:#c00,stroke-width:3px

攻撃者にできること

攻撃者は、JWSを検証するアプリケーションに、任意のペイロード内容を正しく署名されたものとして受け入れさせることができます。それが通常何を意味するかは、アドバイザリとCERT/CCのノートに具体的に記載されています。

  • 認証バイパス:subのようなIDクレームやrole=adminのような権限クレームを偽造しても、本物として通る
  • 認可の偽造:ペイロード内のスコープ、ロール、権限が信頼される
  • 署名付きメッセージの注入:JWS署名付きメッセージで相互に認証するマイクロサービスが、攻撃者の作成したメッセージを受け入れる
  • 完全性のバイパス:送信元の証明をJWS署名に頼るあらゆるデータワークフローが、偽造された内容を受け入れる

攻撃者には鍵も認証情報もユーザー操作も必要ありません。公開されている2つのベクトルがいずれもAV:N/AC:L/PR:N/UI:Nとしているのはそのためです。制約となる条件はアプリケーション側にあります。信頼できない入手元からの一般シリアライゼーションJWSを受け入れ、検証済みとして返された値を信頼している必要があります。

公開されている2つのスコアの違いは可用性への影響のみです。

出典 CVSS v3.1スコア ベクトル 可用性
ベンダーのアドバイザリGHSA-6v38-4h7w-rj9h 9.1、クリティカル CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N なし(A:N)
CVEレコードのCISA ADPエンリッチメント 9.8、クリティカル CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H 高(A:H)

2026年10月1日のCISAのSSVC評価は、Exploitation: none、Automatable: yes、Technical Impact: totalと記録しています。同じエンリッチメントは、この署名検証の失敗をCWE-347(Improper Verification of Cryptographic Signature)に分類しています。

修正が有効な理由

修正はループの前にチェックを一つ追加します。これにより、is_validが初期値のTrueを保てた唯一の形である空のsignatures配列は、フラグをFalseにするようになり、回帰テストがこの挙動を固定します。nullの配列は、もともと検証済みの戻り値に到達していませんでした。Noneを反復すると、修正の前も後もTypeErrorが送出されるためです。

パッチはコミット6c11a02dで、プルリクエスト#938でマージされました。

         headers = []
         is_valid = True
+
+        # invalidates cases like obj["signatures"] = None or obj["signatures"] = []
+        if not obj["signatures"]:
+            is_valid = False
+
         for header_obj in obj["signatures"]:
             jws_header, valid = self._validate_json_jws(
                 payload_segment, payload, header_obj, key

コードコメントはNoneにも言及していますが、その入力に対してチェックが行うのはフラグの設定だけです。直後のforループは従来どおりNoneを反復し、TypeErrorを送出します。したがって、整ったBadSignatureErrorによる拒否が適用されるのは空の配列だけで、Noneには適用されません。

このチェックにより、空の配列ではis_valid = Falseが強制されるため、メソッドはraise BadSignatureError(rv)まで進み、呼び出し元にはトークンが拒否されたことが伝わります。追加されたテストtest_deserialize_json_empty_signaturesは{"payload": "YQ", "signatures": []}を入力し、まさにその例外が送出されることをアサートします。

この経路に対する修正は完全です。少なくとも一つの署名が検証されない限り、攻撃者は検証済みの戻り値に到達できなくなりました。また、署名ごとの検証ループ自体は変更されていないため、署名を持つトークンは従来どおり検証されます。

侵害されたかどうかの確認方法

2026年10月27日時点で、この欠陥に関する侵害の痕跡(IOC)は公開されていません。そのため、以下のチェックはベンダーのIOCリストではなく、欠陥そのものから導いたパターンです。

  • リクエストボディ、トークン受付ログ、Webhookログから、偽造オブジェクトの構造上の特徴を検索する。つまり、signaturesメンバーが空配列であるJWS JSONボディで、たとえば"signatures":[]や"signatures": []です。
  • 受け入れられたJWSのうち、自社側で対応する署名処理がないのにクレームが変わっているものについて、その前後の認証ログと認可ログをレビューする。たとえば、正規の発行者が一度も発行していないIDやロールの値です。
  • 露出期間を確定する。デプロイ先ホストでのpip show authlib、またはdocker exec <container> pip show authlibでインストール済みバージョンがわかります。そのうえでデプロイ履歴を見れば、1.9.0より古いリリースがどれだけの期間稼働していたかがわかり、ログ検索で対象とすべき期間が決まります。

タイムライン

  • 2026-09-23:CERT/CCがCVE-2026-96760を予約(CVEレコードのdateReserved)
  • 2026-09-28:CERT/CCがVU#762428とCVEレコードを公開。このノートは1.7.2までのバージョンを記載し、執筆時点でベンダーと連絡が取れず、パッチも存在しないと述べていた
  • 2026-10-01:修正が作成される(コミット6c11a02dのauthor date)。同日、CISA ADPエンリッチメントがExploitation: noneとして記録される
  • 2026-10-14:Leonardo Oporto氏によるプルリクエスト#938の修正を含むAuthlib v1.9.0がリリースされる
  • 2026-10-27:GitHub Advisory Databaseが14:50 UTCにレビュー済みエントリーGHSA-6v38-4h7w-rj9hを公開し、報告者としてzer0d4y5、uziii2208、nguyenminhtuan28の各氏をクレジット。本記事はそれに続くもの

よくある質問

通常のコンパクトシリアライゼーションのJWTは影響を受けるか

いいえ。ドット区切りのheader.payload.signature形式はdeserialize_compact()を通り、これは常にalgorithm.verify()を呼び出し、不一致があればBadSignatureErrorを送出します。影響を受けるのは、signatures配列が空の一般JSONシリアライゼーション形式だけです。

このバグが問題になるのはアプリケーションが鍵を渡さない場合だけか

いいえ。検証鍵は_validate_json_jws()の内側で使われ、空のループはこれを一度も呼び出しません。そのため、アプリケーションが有効な鍵を正しく渡していても、偽造トークンはすり抜けます。「悪用方法」のエクスプロイトのコード例は、deserialize_json()をkey=Noneで呼び出したあと、本物の鍵で再度呼び出しており、アドバイザリの再現でも、どちらの場合も偽造オブジェクトが受け入れられています。本物の鍵を渡しているアプリケーションこそが脆弱な対象です。

フラット化JSON形式は影響を受けるか

いいえ。signaturesではなくsignatureメンバーを持つオブジェクトはフラット化の分岐を通り、単一の署名を検証して、失敗すれば例外を送出します。このバグは、一般形式のフラグ初期化と空の配列の組み合わせによるものです。

実際に悪用されているか

既知の悪用はありません。2026年10月1日のCISAのSSVC評価はExploitation: noneと記録しており、このCVEは2026年10月27日時点でCISA KEVカタログに掲載されていません。アドバイザリの公開以降、公開PoCが存在するため、露出の確認にあたっては、誰でもこの手法を再現できると想定すべきです。

CERT/CCが1.7.2を挙げ、アドバイザリが1.8.0を挙げているのはなぜか

2026年9月28日のCERTのノートは1.7.2までのバージョンを挙げ、その時点ではパッチが存在しなかったと記録しています。バージョン1.8.0は2026年8月30日にすでにリリースされていましたが、先のノートが1.7.2で止まっている理由は、出典では説明されていません。正式な範囲はベンダーのアドバイザリGHSA-6v38-4h7w-rj9hです。1.8.0までのすべてのリリースが対象で、最初の修正は1.9.0であり、アドバイザリ自身の再現は1.8.0で観測されています。

アップグレードにコードの変更は必要か

いいえ。修正はdeserialize_json()の内部で完結しているため、pip install --upgrade "authlib>=1.9.0"を実行してデプロイ済みのコンテナを再作成すれば、欠陥は解消されます。1.9.0は空のsignaturesに対してフェイルクローズするため、事前フィルターの回避策を残しているアプリケーションは、アップグレード後にそれを削除できます。

攻撃者より先に脆弱なAuthlibのインストールを見つける

リポジトリとコンテナイメージに対してOstorlab SCAを実行し、1.9.0未満のAuthlibをまだ含んでいるすべてのプロジェクトとリリースを一覧化しましょう。

参考資料

  • ベンダーのアドバイザリGHSA-6v38-4h7w-rj9h。根本原因、PoC、1.8.0までの影響範囲、1.9.0の修正を記載:https://github.com/authlib/authlib/security/advisories/GHSA-6v38-4h7w-rj9h(2026年10月27日確認)
  • GitHub Advisory Databaseのレビュー済みエントリーGHSA-6v38-4h7w-rj9h。クリティカル、CVSS v3.1 9.1(CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N)、2026年10月27日公開、zer0d4y5、uziii2208、nguyenminhtuan28の各氏をクレジット:https://github.com/advisories/GHSA-6v38-4h7w-rj9h(2026年10月27日確認)
  • CVE-2026-96760のレコード。CNAはCERT/CC、2026年9月28日公開、CISA ADPエンリッチメント付き:CVSS v3.1 9.8(CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H)、CWE-347(Improper Verification of Cryptographic Signature)、2026年10月1日時点でSSVC Exploitation: none:https://cveawg.mitre.org/api/cve/CVE-2026-96760(2026年10月27日確認)
  • CERT/CC Vulnerability Note VU#762428。初版公開は2026年9月28日。空のsignaturesによるバイパス、影響を受ける2つのエントリーポイント、影響シナリオ、公開時点でパッチがなかったことを記載:https://www.kb.cert.org/vuls/id/762428(2026年10月27日確認)
  • 修正コミット6c11a02d「Fix empty signatures validation in JWS JSON deserialization」。2026年10月1日にLeonardo Oporto氏が作成し、追加されたガードと回帰テストを含む:https://github.com/authlib/authlib/commit/6c11a02da8730914cea08ec195f1108a40dc6733(2026年10月27日確認)
  • 修正のプルリクエスト#938:https://github.com/authlib/authlib/pull/938(v1.9.0のリリースノートから参照、2026年10月27日確認)
  • 修正を含むAuthlib v1.9.0のリリース。2026年10月14日公開:https://github.com/authlib/authlib/releases/tag/v1.9.0(2026年10月27日確認)
  • タグv1.8.0の脆弱なauthlib/jose/rfc7515/jws.py。deserialize_json()、deserialize()、_validate_json_jws()の実装:https://raw.githubusercontent.com/authlib/authlib/v1.8.0/authlib/jose/rfc7515/jws.py(2026年10月27日確認)
  • AuthlibのPyPIダウンロード数。直近1週間で28,498,200、直近1か月で107,757,780:https://pypistats.org/api/packages/authlib/recent(2026年10月27日確認)
  • CISA Known Exploited Vulnerabilitiesカタログ。2026年10月27日にCVE-2026-96760を確認(掲載なし):https://www.cisa.gov/known-exploited-vulnerabilities-catalog