On 9 September 2026 the Authlib project published GHSA-4pp3-559j-c5c2 for joserfc, its Python library for JOSE standards (JWS, JWE, JWK and JWT). The bug, later assigned CVE-2026-102946, is rated Critical with CVSS 3.1 9.1. joserfc.jws.deserialize_json() accepted a JWS in General JSON Serialization whose signatures array was empty, and returned the payload as authenticated. No key needed.
Two more advisories followed on 6 October, both resource exhaustion bugs fixed in 1.7.5.
What's affected
| Advisory | Issue | Severity | Affected | Fixed |
|---|---|---|---|---|
| GHSA-4pp3-559j-c5c2 / CVE-2026-102946 | Empty signatures array accepted as verified | Critical, 9.1 | 1.7.3 (confirmed), likely earlier | 1.7.4 |
| GHSA-jpxq-7h8p-2q76 | Unbounded JWE recipients array, one RSA decryption per entry | High, 7.5 | 1.7.4 | 1.7.5 |
| GHSA-9pg5-8c7x-5hfx | Compact JWS size guard copies segments before rejecting | Moderate | 1.7.4 and earlier | 1.7.5 |
For the signature bypass, the advisory is precise about what's in scope. Affected: jws.deserialize_json() on the General JSON Serialization branch. Not affected: jws.deserialize_compact(), jwt.decode() (which uses the compact path), Flattened JSON (a missing signature raises an error) and JWE JSON.
So the common case of decoding a compact JWT access token is not exposed. The risk sits in apps that verify JSON-serialized JWS, for example signed webhooks, signed documents and multi-signature payloads.
The root cause is a classic loop bug. verify_general_json() returned True if no signature failed. With an empty array, the loop ran zero times, nothing failed, and the result was "verified." The reporter's proof of concept:
from joserfc import jws
from joserfc.jwk import OctKey
server_key = OctKey.import_key("correct horse battery staple")
forged = {"payload": "eyJ1c2VyIjoiYWRtaW4ifQ", "signatures": []}
token = jws.deserialize_json(forged, server_key) # expected: BadSignatureError
print(token.payload) # actual: b'{"user":"admin"}'
The reporter, credited as messa, noted the issue was found during AI-assisted code review.
The JWE bug is a cost amplifier. Each entry in recipients triggered a full RSA private-key decryption. The reporter measured about 3.76 seconds of CPU for a 1.19 MB request with 3,000 recipients on RSA-2048, and showed that the documented verify_all_recipients=False setting didn't reduce the work.
How to check if you're exposed
Find the version:
pip show joserfc 2>/dev/null | grep -i ^version
pip-audit -r requirements.txt 2>/dev/null | grep -i joserfc
Then check whether you call the affected functions:
grep -rnE "deserialize_json|decrypt_json" --include="*.py" .
If neither appears and you only use jwt.decode() or deserialize_compact(), the 9.1 bypass doesn't reach your code. You still want 1.7.5 for the compact JWS memory issue if you accept tokens from untrusted clients.
If you'd rather not run tooling, the free scan at https://gold.safeguard.sh/scan accepts a requirements.txt in the browser, and the file never leaves your machine.
What to do now
-
Upgrade to joserfc 1.7.5 or later. That covers all three advisories.
pip install --upgrade "joserfc>=1.7.5" -
If you verify JSON-serialized JWS, add your own guard until every service is upgraded: reject any input where
signaturesis missing or empty before calling the library. -
Cap request sizes on endpoints that accept JWE or JWS, at the web server or framework level, so a 1 MB token is rejected before it reaches Python.
-
Review what a forged payload could have done. If a webhook or signed document endpoint used
deserialize_json()on an affected version, check its logs for requests whose JSON body had an emptysignaturesarray. -
Add a negative test to your own suite: a JWS with no signatures must fail verification. It's a one-line test that would have caught this.
Why it matters
"All signatures valid" and "at least one signature valid" are different checks, and an empty set satisfies the first one. The same pattern shows up in SAML libraries that validate every signed element but never require one to exist, and in policy engines that pass when no rule matches. Wherever your code loops over proofs, ask what happens when there are none.
The second lesson is about multi-recipient formats. JSON serializations of JWS and JWE let one message carry many signatures or recipients, and every entry is work the server does for an unauthenticated caller. Libraries are adding caps, and your edge should too.
Try it yourself
Search your Python services for deserialize_json and decrypt_json. For each hit, write down whether the input comes from an untrusted party. If none of your code calls them, the critical bug is present in your dependency tree but not reachable, which is the distinction the demo below shows.
Click through it at your own pace, or open it full screen.
Quick check
We only call jwt.decode() on compact access tokens. Are we exposed to CVE-2026-102946?
Not to the signature bypass. The advisory says jwt.decode() uses the compact path, which always runs signature verification. Upgrade to 1.7.5 anyway for the memory issue in compact JWS parsing.
Does setting verify_all_recipients=False mitigate the JWE DoS?
No. The advisory says it only changes whether a decrypt failure raises an error. Every recipient is still decrypted first.
Which version fixes everything listed here?
joserfc 1.7.5.