Someone left the bank vault door hinged on a piece of string — but only the side entrance that 5% of customers use
CVE-2026-96760 is a signature-verification bypass in the authlib.jose module (all versions through 1.8.0, fixed in 1.9.0). The JsonWebSignature.deserialize_json() method — and the generic jws.deserialize() dispatcher when it routes to the JSON path — initialises an is_valid flag to True *before* iterating through the signatures array. When an attacker supplies a JWS object with an empty signatures array, the loop body never executes, no key material is checked, and the payload is returned as cryptographically verified. That payload can carry arbitrary identity or authorisation claims (sub, role, scope, tenant), giving an unauthenticated remote caller a complete authentication bypass — zero keys, zero credentials, one HTTP request. The critical detail: the compact JWT path (jwt.decode()) — which is how the overwhelming majority of OAuth/OIDC deployments consume Authlib — is not affected.
The vendor score of CVSS 9.8 CRITICAL is technically correct *for applications that call the vulnerable methods*, but it misrepresents the realistic exposure across the Authlib installed base. The CVSS vector (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H) assumes every consumer sits on the vulnerable path; in practice, most Authlib users validate compact JWTs, not General JSON Serialization JWS objects. The authlib.jose module has been deprecated since v1.7.0 in favour of joserfc, further shrinking the exposed population. CERT/CC published advisory VU#762428 on 2026-10-13 after failing to coordinate with the maintainer. The maintainer later confirmed affected scope extends through 1.8.0 (not just 1.7.2 as the original CVE text states) and shipped the fix in 1.9.0. For the subset of applications that *do* consume JSON-serialized JWS, the vendor score is entirely fair — this is a one-shot, zero-skill, no-key auth bypass. For everyone else, it is an academic exposure in a deprecated module they never call.
4 steps from start to impact.
Identify target consuming JSON-serialized JWS
signatures arrays, content negotiation headers, or error messages revealing Authlib stack traces. The attacker confirms the target runs Authlib ≤ 1.8.0 — identifiable through dependency metadata, error strings, or response timing.- Target application uses Authlib ≤ 1.8.0
- Target application calls
jws.deserialize_json()orjws.deserialize()with user-controlled JSON input - Target endpoint is network-reachable
- The vast majority of Authlib consumers use
jwt.decode()(compact JWT) which is NOT vulnerable - The
authlib.josemodule has been deprecated since v1.7.0; many projects have migrated tojoserfc - JSON Serialization JWS is a niche format — most OAuth/OIDC token flows use compact JWTs
authlib<1.9.0. pip audit detects the advisory via GHSA-6v38-4h7w-rj9h.Craft JWS with empty signatures array
payload containing the desired identity/authorization claims (e.g., {"sub":"admin","role":"superuser"}) and an empty signatures array. No cryptographic key material is needed. The PoC by uziii2208 demonstrates this in under 10 lines of Python. The exploit payload is a plain JSON document — nothing exotic.- Knowledge of the expected claim schema (field names like
sub,role,scope)
- Trivial — the PoC is public and the payload is a simple JSON object
- Claim schema can often be inferred from API docs, error messages, or a legitimate token
"signatures":[], but no mainstream WAF ships this rule by default.Submit forged JWS to the application endpoint
deserialize_json() method processes the empty signatures array, never enters the verification loop, and returns the attacker's payload as verified. No rate-limiting or authentication check intervenes because the bypass happens *inside* the token validation layer.- HTTP(S) network access to the target endpoint
- None — standard HTTP request
sub=admin from an unknown source). SIEM correlation on impossible-travel or privilege-escalation patterns could catch post-exploitation.Achieve full authentication and authorization bypass
- Application makes authorization decisions based on JWS payload claims
- None once the forged payload is accepted
deserialize_json(). Run pip install --upgrade 'authlib>=1.9.0' across all environments. Per the noisgate mitigation SLA for HIGH, deploy within 30 days. Consider 1.9.1 which also addresses CVE-2026-114616.authlib.jose module is deprecated. Migrate JWS/JWT operations to the standalone joserfc package (maintained by the same author). This eliminates the entire vulnerable module from your dependency tree and future-proofs against further authlib.jose bugs. This is the recommended long-term path even after upgrading.JsonWebSignature.deserialize_json(), jws.deserialize_json(), and jws.deserialize(). If your application only uses jwt.decode() or higher-level OAuth client methods, you are not on the vulnerable path and the practical risk is negligible. Document the finding. If you find usage, prioritize those services for immediate upgrade."signatures" before it reaches Authlib. A simple string check or JSON schema validation at the API gateway or application layer breaks the attack chain at step 3. This is a stopgap — not a replacement for upgrading.- Generic WAF rules — The forged JWS is a syntactically valid JSON document. No mainstream WAF ships a rule for empty
signaturesarrays in JWS payloads. You would need a custom rule, and even then the payload can be delivered in headers, bodies, or query parameters. - Network segmentation / firewall rules — The attack arrives as a normal HTTP(S) request to the application's API endpoint. Network-level controls cannot distinguish it from legitimate traffic.
- MFA / step-up authentication — The bypass occurs at the token validation layer, *below* any user-facing authentication ceremony. MFA protects the login flow; this bug forges the token that login produces.
- TLS / mTLS — Transport encryption protects data in transit but does not validate the integrity of JWS payloads at the application layer. mTLS authenticates the *transport peer*, not the *token issuer*.
The supporting signals.
| In-the-Wild Exploitation | No confirmed exploitation. Not listed in CISA KEV. No GreyNoise or Shadowserver scanning signatures observed as of 2026-10-28. The niche code path (JSON Serialization JWS) limits mass-exploitation appeal. |
|---|---|
| Proof-of-Concept | Public PoC available. GitHub repo uziii2208/CVE-2026-96760 by researchers uziii2208 and nguyenminhtuan28 includes exploit.py demonstrating the bypass with three test cases (legitimate sig accepted, wrong key rejected, empty-signatures bypass succeeds). Trivial to weaponize. |
| EPSS | 0.00383 (~96th percentile inverted — low exploitation probability). Reflects the niche attack surface and lack of observed exploitation. |
| CISA KEV | Not listed as of 2026-10-28. |
| CVSS Vector | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H (9.8 Critical). The vector is accurate *for the vulnerable code path* — network-reachable, zero complexity, no auth, full CIA impact. Overstates risk for the broader Authlib installed base because it does not model the niche-path friction. |
| Affected Versions | All Authlib versions ≤ 1.8.0 (maintainer-confirmed scope via GHSA-6v38-4h7w-rj9h). Original CVE text says ≤ 1.7.2 but the maintainer expanded scope. The authlib.jose module was deprecated in 1.7.0 but not fully removed until 1.8+. |
| Fixed Version | Authlib 1.9.0 (also 1.9.1 which includes additional fixes for CVE-2026-114616). Upgrade path: pip install --upgrade authlib>=1.9.0. Alternative: migrate to standalone joserfc package. |
| Installed Base | ~107M monthly PyPI downloads, 1,140 direct dependents, 1.69B total downloads. Used by Google Cloud Platform projects, Streamlit, GNS3, and numerous OAuth/OIDC integrations. Widely depended upon as a transitive dependency via FastMCP and similar frameworks. |
| CERT/CC Advisory | VU#762428 published 2026-10-13. CERT/CC notes they could not reach the Authlib maintainers for coordinated disclosure. Fix was shipped independently by the maintainer. |
| Disclosure Timeline | 2026-07-28: reported to maintainer → 2026-07-30: closed without fix (citing deprecation) → 2026-09-23–28: CVE-2026-96760 assigned and published → 2026-10-13: CERT/CC VU#762428 published → 2026-10-xx: Authlib 1.9.0 released with fix. |
Sources.
Why this verdict
- Niche code path limits reachable population. The vulnerability lives exclusively in the JSON Serialization JWS path (
deserialize_json()/deserialize()routing to JSON). The compact JWT path (jwt.decode()), which is the dominant consumption pattern for OAuth/OIDC token validation across the Authlib installed base, is not affected. Estimated reachable population: 2–10% of Authlib consumers. This is the single largest downward pressure from 9.8. - Deprecated module further shrinks exposure. The
authlib.josemodule has been deprecated since v1.7.0 (May 2026) with migration guidance tojoserfc. Projects that followed the deprecation warning are not vulnerable. This compounds the code-path friction — the vulnerable surface is a deprecated method inside a deprecated module. - Zero-skill, zero-key exploit when path IS hit. For the minority of applications that do consume JSON-serialized JWS via Authlib, the exploit is trivially weaponisable: one HTTP request, no cryptographic material, complete auth bypass. The public PoC by uziii2208 confirms this. This prevents any downgrade below HIGH.
- Role multiplier: Authlib IS canonically identity infrastructure — it is *the* authentication library for Python OAuth/OIDC. Applications that use the vulnerable JSON JWS path for token validation (e.g., custom OAuth servers, microservice auth gateways, API platforms) occupy the identity-provider high-value role. When the chain succeeds in this role, the blast radius is identity-scale: arbitrary impersonation, privilege escalation, and token forgery. This sets a HIGH floor. However, the CRITICAL floor (≥10% of installs in high-value role) does not engage because the vulnerable JSON Serialization path represents <10% of Authlib's usage pattern — the canonical identity-role usage is compact JWT which is safe.
- No in-the-wild exploitation and low EPSS (0.00383) provide minor downward signal but are not dispositive — the PoC is public and trivial, and exploitation would be difficult to distinguish from legitimate auth traffic at the network level.
Why not higher?
The CRITICAL floor requires ≥10% of the affected component's installed base to canonically occupy the high-value role *on the vulnerable code path*. Authlib is identity infrastructure, but the vulnerable JSON Serialization JWS path is a niche feature — the canonical usage (jwt.decode() for compact JWTs) is safe. With <10% of Authlib consumers using the vulnerable path, the CRITICAL floor does not engage. There is also no confirmed in-the-wild exploitation to override via KEV.
Why not lower?
The vulnerability is a complete, zero-skill authentication bypass requiring no credentials or key material — the most dangerous class of identity flaw. Authlib has 107M+ monthly downloads and 1,140 dependents; even at 2–5% vulnerable-path usage, this represents thousands of applications. The public PoC makes exploitation trivial for any attacker who identifies a target. The identity-infrastructure role means successful exploitation yields impersonation and privilege escalation, not just information disclosure. The HIGH floor holds firmly.
Crowdsourced verification payload.
Run on any host where the target application's Python environment is accessible. Execute with python3 check_cve_2026_96760.py — no special privileges required, just access to the virtualenv or site-packages where authlib is installed. Safe to run in production; it performs no destructive operations.
#!/usr/bin/env python3
"""CVE-2026-96760 checker: Authlib JWS signature verification bypass.
Outputs: VULNERABLE / PATCHED / UNKNOWN
Exit codes: 1=VULNERABLE, 0=PATCHED, 2=UNKNOWN
"""
import sys
import json
import base64
def b64url(data):
return base64.urlsafe_b64encode(data.encode()).rstrip(b'=').decode()
def main():
# Step 1: Check if authlib is installed and get version
try:
import authlib
except ImportError:
print("UNKNOWN - authlib is not installed in this environment")
sys.exit(2)
version = getattr(authlib, '__version__', 'unknown')
print(f"[*] Detected authlib version: {version}")
# Step 2: Version-based check
try:
parts = version.split('.')
major, minor = int(parts[0]), int(parts[1])
patch = int(parts[2]) if len(parts) > 2 else 0
if major > 1 or (major == 1 and minor >= 9):
print(f"PATCHED - authlib {version} >= 1.9.0 (CVE-2026-96760 fixed)")
sys.exit(0)
except (ValueError, IndexError):
print(f"[!] Could not parse version '{version}', falling back to runtime test")
# Step 3: Check if the vulnerable module exists
try:
from authlib.jose import JsonWebSignature
except ImportError:
print("PATCHED - authlib.jose module not present (removed in this build)")
sys.exit(0)
# Step 4: Runtime exploit test (non-destructive)
print("[*] Testing deserialize() with empty signatures array...")
jws = JsonWebSignature()
forged_payload = b64url(json.dumps({"sub": "cve-2026-96760-test", "role": "checker"}))
forged_jws = json.dumps({"payload": forged_payload, "signatures": []})
try:
result = jws.deserialize(forged_jws, key=None)
if result is not None:
payload_bytes = result.get('payload', b'')
decoded = payload_bytes.decode() if isinstance(payload_bytes, bytes) else str(payload_bytes)
print(f"VULNERABLE - empty-signatures JWS accepted without key material")
print(f"[!] Forged payload returned as verified: {decoded}")
sys.exit(1)
else:
print("PATCHED - deserialize() returned None for empty signatures")
sys.exit(0)
except TypeError:
# Some versions raise TypeError when key=None and signatures exist
print("PATCHED - deserialize() rejected empty signatures (TypeError)")
sys.exit(0)
except Exception as e:
ename = type(e).__name__
# Any exception means the exploit did not succeed
if 'signature' in str(e).lower() or 'verify' in str(e).lower() or 'invalid' in str(e).lower():
print(f"PATCHED - deserialize() rejected empty signatures ({ename}: {e})")
sys.exit(0)
print(f"UNKNOWN - unexpected error during runtime test ({ename}: {e})")
sys.exit(2)
if __name__ == "__main__":
main()