Skip to content

GHSA-hxm8-2xgr-2p9m CVE-2026-102269 PYSEC-2026-4147

PyJWT: Non-canonical signature segments enable raw-token revocation bypass

Medium PyPI
Severity
4.8 / 10
Medium · CVSS 3.1
Exploit chance (EPSS)
0.22%
Chance of exploitation in the next 30 days. Higher than 13.02% of scored vulnerabilities.
Known exploited
No
Not in CISA's catalog
Weakness
CWE-180 Incorrect Behavior Order: Validate Before Canonicalize
Fix
2.14.0
Upgrade pyjwt to this version or later

Description

Summary

PyJWT 2.13.0 accepts compact JWS signature segments containing characters that are not in the Base64URL alphabet. Appending !!!! to a valid signature does not change the decoded signature bytes or authenticated claims, but it changes the serialized token and its SHA-256 hash. An application that indexes logout or revocation state by the raw token therefore rejects the logged-out token and accepts its no-key alternate serialization.

This report separates two issues: permissive compact-JWS decoding is the library behavior, while using raw serialized JWT text as a security identity is a dangerous application composition. A jti-indexed control blocks both representations.

Affected Version

Confirmed with PyJWT 2.13.0 on CPython 3.13. The mutation changes only the signature segment and requires no signing key.

End-to-End Reproduction

.venv-research/bin/python 06-practical-testing/revocation-bypass-lab.py

The loopback-only lab performs:

  1. Issue a valid HS256 admin token.
  2. Access /protected successfully.
  3. Log out the canonical token and store SHA256(raw_token).
  4. Confirm canonical replay returns HTTP 401.
  5. Append !!!! to only the signature segment.
  6. Confirm the mutated token has the same verified claims and signature bytes, a different raw hash, and receives HTTP 200.
  7. Repeat with jti-indexed revocation and confirm both forms return HTTP 401.

Observed statuses for raw-token revocation were 200 -> logout -> 401 for the canonical token and 200 for the alternate serialization. The jti control returned 401 for both replays after logout.

Security Boundaries and Severity

This is not signature forgery: the attacker starts with a valid token and cannot alter its claims. Impact requires a consumer to bind revocation, replay state, rate limits, or cache identity to the raw compact string. Under that composition, an authenticated user can bypass logout or token-specific revocation without the signing key. Medium is suggested for triage because the security impact is real but application-dependent.

Recommended Fix

  • Reject signature segments containing characters outside [A-Za-z0-9_-].
  • Reject padding and other non-canonical Base64URL encodings for compact JWS.
  • Add regression tests proving malformed signature encodings fail before or during verification.
  • Document that applications must use a semantic identifier such as jti, not the raw serialized token, for revocation and replay state.

Maintainer update — 2026-09-09

We reproduced the reported behavior on the verified master commit 8915570: appending !!!! to a valid compact-JWS signature segment is accepted, produces the same decoded signature bytes and claims, and changes the serialized token hash. Existing JWS verification controls pass. The behavior is present in the tested release range represented by tags 2.4.0, 2.8.0, 2.10.1, 2.12.1, and 2.13.0.

The security impact is conditional on an application using the raw serialized token as the identity for logout, revocation, replay, rate-limit, or cache state. Under that composition, an attacker who already holds a valid token can use an alternate serialization after the canonical string is revoked. This is not signature forgery or claim alteration; applications should use a semantic identifier such as jti for token identity.

The report is ready to be accepted as a draft for continued remediation. A narrow fix assessment recommends rejecting non-canonical Base64URL characters and padding in compact-JWS segments. No fix commit or release is claimed yet.

Maintainer update — 2026-09-09

The fix has been rebased onto the current master as e6f48401001609a8f99e71fcaf355fb895d508a8 and is ready to be pushed. Compact-JWS header, payload, and signature segments are now required to use the unpadded Base64URL alphabet and canonical re-encoding; a signature segment containing non-canonical characters such as !!!! is rejected before verification. Valid detached b64=false handling remains supported.

Verification on the rebased fix commit passed 372 tests with 4 intentional cryptography-environment skips; Ruff formatting/lint and git diff --check passed. The compressed-payload regression fixture was updated to valid canonical compact-JWS encoding. No release containing the fix has been published yet, so the patched version remains unset pending release planning.

The advisory remains medium severity and unpublished.

Classification update — 2026-09-10

We completed the advisory classification review. The proposed CVSS 3.1 vector is CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:N and the proposed CWE classification is CWE-180. The issue permits alternate non-canonical serializations of an already valid signed token to bypass applications that identify tokens by their raw serialization; it does not forge signatures or alter claims. CWE-180 captures validation/canonicalization ordering.

These metadata changes do not alter the affected range, fix status, or lifecycle state.

Maintainer update — 2026-09-11

The verified fix for this advisory is included in PyJWT 2.14.0, released on 2026-09-11 and available on PyPI. PyJWT 2.14.0 is the first release containing the fix. This advisory is now published with 2.14.0 recorded as the patched version.

Affected versions

PyPI

PackageVulnerableFixed in
pyjwt
< 2.14.0
2.14.0

How to fix

Move to the fixed version or later with your package manager, update the lockfile, and run your tests. If the package only arrives through another dependency, upgrade that dependency first. If none of its releases pulls a fixed version yet, force the version with your package manager's override.

pyjwt PyPI → 2.14.0

pip
pip install --upgrade "pyjwt>=2.14.0"
poetry
poetry add "pyjwt>=2.14.0"
uv
uv add "pyjwt>=2.14.0"

Severity breakdown

CVSS 3.1 4.8

CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:N

Attack vector
Network
Attack complexity
High
Privileges required
None
User interaction
None
Scope
Unchanged
Confidentiality
Low
Integrity
Low
Availability
None

Weakness

CWE-180: Incorrect Behavior Order: Validate Before Canonicalize

The product validates input before it is canonicalized, which prevents the product from detecting data that becomes invalid after the canonicalization step.

How to prevent it
  • Implementation: Inputs should be decoded and canonicalized to the application's current internal representation before being validated (CWE-180). Make sure that the application does not decode the same input twice (CWE-174). Such errors could be used to bypass allowlist validation schemes by introducing dangerous inputs after they have been checked.

References

Timeline

  1. Published
  2. Last updated by the source
  3. Latest EPSS score
AdvisorySeverityWhy it's related
GHSA-pxh4-856f-4h89
PyJWT: HS256/384/512 verify accepts empty HMAC key via oct JWK (PyJWK/PyJWKClient) enabling token forgery
High Also affects pyjwt
GHSA-gvp8-978c-rx2q
PyJWT.decode() reintroduces options-dict mutation, enabling silent claim-verification bypass on dict reuse
Medium Also affects pyjwt
GHSA-x33g-cr3x-6449
PyJWT accepts inconsistent OKP x/d JWKs, causing public/private key identity confusion
High Also affects pyjwt
GHSA-w6j9-cwv2-h6wq
PyJWT: Malformed RSA JWK aborts parsing of an entire JWK Set
High Also affects pyjwt
GHSA-w2cx-738m-mc7w
PyJWT accepts public JWK containers as HMAC secrets
High Also affects pyjwt
GHSA-r6x4-923q-g947
PyJWT BOM Bypass
High Also affects pyjwt
GHSA-p4g4-x82p-q773
PyJWT: Public keys in DER form are accepted as HMAC secrets, bypassing the CVE-2022-29217 guard
High Also affects pyjwt
GHSA-jwrc-g2q2-pq5p
PyJWT: ReDoS vulnerability when calling the `is_pem_format` function.
Medium Also affects pyjwt
Is this in your code?

MergeHarbor checks every repository's dependencies against this database, ranks what matters by exploitability and reachability, routes it to the owning team and opens the fix pull request.

Check your repos with MergeHarbor