Safeguard
Vulnerability Analysis•

joserfc CVE-2026-102946: An Empty Signatures Array Passed as Signed

joserfc's JWS JSON deserializer accepted a message with zero signatures as verified. Fixed in 1.7.4, with two DoS fixes following in 1.7.5.

Safeguard Security Team
5 min read

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

AdvisoryIssueSeverityAffectedFixed
GHSA-4pp3-559j-c5c2 / CVE-2026-102946Empty signatures array accepted as verifiedCritical, 9.11.7.3 (confirmed), likely earlier1.7.4
GHSA-jpxq-7h8p-2q76Unbounded JWE recipients array, one RSA decryption per entryHigh, 7.51.7.41.7.5
GHSA-9pg5-8c7x-5hfxCompact JWS size guard copies segments before rejectingModerate1.7.4 and earlier1.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

  1. Upgrade to joserfc 1.7.5 or later. That covers all three advisories.

    pip install --upgrade "joserfc>=1.7.5"
    
  2. If you verify JSON-serialized JWS, add your own guard until every service is upgraded: reject any input where signatures is missing or empty before calling the library.

  3. 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.

  4. 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 empty signatures array.

  5. 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.

Sources

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.