← Back to Feed CACHED · 2026-10-27 20:10:30 · CACHE_KEY CVE-2026-96760
CVE-2026-96760 · CWE-20 · Disclosed 2026-09-28

Authlib JWS signature verification bypass via empty signatures array

ASSESSED — NOISGATE
Vendor
—
—
—
Reassessed
—
—
—
Verdict: —
Do you agree?
01 · The Real Story

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.

"Trivial auth bypass on a niche code path: deadly if you hit it, but most Authlib callers don't"
02 · The Attack Path

4 steps from start to impact.

STEP 01

Identify target consuming JSON-serialized JWS

The attacker fingerprints a target web application or API that accepts JWS tokens via the General JSON Serialization format (RFC 7515 §7.2) rather than the far more common Compact Serialization. Indicators include API documentation referencing 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.
Conditions required:
  • Target application uses Authlib ≤ 1.8.0
  • Target application calls jws.deserialize_json() or jws.deserialize() with user-controlled JSON input
  • Target endpoint is network-reachable
Where this breaks in practice:
  • The vast majority of Authlib consumers use jwt.decode() (compact JWT) which is NOT vulnerable
  • The authlib.jose module has been deprecated since v1.7.0; many projects have migrated to joserfc
  • JSON Serialization JWS is a niche format — most OAuth/OIDC token flows use compact JWTs
Detection/coverage: SCA/SBOM tools (Dependabot, Snyk, Trivy, Safety) will flag authlib<1.9.0. pip audit detects the advisory via GHSA-6v38-4h7w-rj9h.
STEP 02

Craft JWS with empty signatures array

The attacker constructs a JSON object with a base64url-encoded 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.
Conditions required:
  • Knowledge of the expected claim schema (field names like sub, role, scope)
Where this breaks in practice:
  • 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
Detection/coverage: Deep-packet-inspection or WAF rules *could* flag a JWS with "signatures":[], but no mainstream WAF ships this rule by default.
STEP 03

Submit forged JWS to the application endpoint

The attacker sends a standard HTTP request to the vulnerable endpoint, placing the forged JWS in the expected location (Authorization header, request body, or query parameter depending on the application's integration). The 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.
Conditions required:
  • HTTP(S) network access to the target endpoint
Where this breaks in practice:
  • None — standard HTTP request
Detection/coverage: Application access logs would show the forged identity (e.g., sub=admin from an unknown source). SIEM correlation on impossible-travel or privilege-escalation patterns could catch post-exploitation.
STEP 04

Achieve full authentication and authorization bypass

The application trusts the 'verified' payload and grants the attacker whatever access the forged claims describe — admin panels, privileged API operations, data exfiltration endpoints, or inter-service trust boundaries. In an OAuth server scenario, the attacker mints arbitrary tokens. In a microservice mesh, the attacker impersonates any service. The blast radius is bounded only by what the application does with the JWS claims.
Conditions required:
  • Application makes authorization decisions based on JWS payload claims
Where this breaks in practice:
  • None once the forged payload is accepted
Detection/coverage: Anomaly detection on access patterns (new admin session from unknown IP, privilege escalation without prior login) is the primary post-exploitation signal.
03 · Compensating Control

1
HIGH 8.1→IGNORE 0.0
SEVERITY REDUCED
Upgrade Authlib to ≥ 1.9.0 — The definitive fix. Version 1.9.0 patches the empty-signatures logic in 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.
2
HIGH 8.1→IGNORE 0.0
SEVERITY REDUCED
Migrate from authlib.jose to joserfc — The 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.
3
HIGH 8.1→LOW 2.0
SEVERITY REDUCED
Audit codebase for deserialize_json and deserialize usage — Search your codebase for calls to 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.
4
HIGH 8.1→LOW 2.5
SEVERITY REDUCED
Add input validation to reject JSON-serialized JWS — If your application expects compact JWTs (the common case), add a pre-validation check that rejects any input containing "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.
What doesn't work
  • Generic WAF rules — The forged JWS is a syntactically valid JSON document. No mainstream WAF ships a rule for empty signatures arrays 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*.
04 · Intelligence Metadata

The supporting signals.

In-the-Wild ExploitationNo 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-ConceptPublic 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.
EPSS0.00383 (~96th percentile inverted — low exploitation probability). Reflects the niche attack surface and lack of observed exploitation.
CISA KEVNot listed as of 2026-10-28.
CVSS VectorCVSS: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 VersionsAll 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 VersionAuthlib 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 AdvisoryVU#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 Timeline2026-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.

  1. CERT/CC Advisory VU#762428
  2. GitHub Security Advisory GHSA-6v38-4h7w-rj9h
  3. PoC by uziii2208 — CVE-2026-96760
  4. CyberNews — Authlib authentication bypass flaw
  5. Authlib Changelog (1.8.0 documentation)
  6. Authlib PyPI — download statistics and version history
  7. OpenCVE — CVE-2026-96760 details
05 · The Call

Final Verdict
↓ DOWNGRADED to HIGH (8.1/10)

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.jose module has been deprecated since v1.7.0 (May 2026) with migration guidance to joserfc. 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.

06 · Verification

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.

noisgate-verify.py
PYTHONREAD-ONLYSAFE
#!/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()