Skip to content

fix(deps): update dependency pyjwt to v2.12.0 [security]#589

Open
renovate[bot] wants to merge 1 commit intomainfrom
renovate/pypi-pyjwt-vulnerability
Open

fix(deps): update dependency pyjwt to v2.12.0 [security]#589
renovate[bot] wants to merge 1 commit intomainfrom
renovate/pypi-pyjwt-vulnerability

Conversation

@renovate
Copy link
Contributor

@renovate renovate bot commented Mar 14, 2026

This PR contains the following updates:

Package Change Age Confidence
pyjwt >=2.9.0,<2.10 >=2.12,<2.13 age confidence
pyjwt 2.10.12.12.0 age confidence

GitHub Vulnerability Alerts

CVE-2026-32597

Summary

PyJWT does not validate the crit (Critical) Header Parameter defined in
RFC 7515 §4.1.11. When a JWS token contains a crit array listing
extensions that PyJWT does not understand, the library accepts the token
instead of rejecting it. This violates the MUST requirement in the RFC.

This is the same class of vulnerability as CVE-2025-59420 (Authlib),
which received CVSS 7.5 (HIGH).


RFC Requirement

RFC 7515 §4.1.11:

The "crit" (Critical) Header Parameter indicates that extensions to this
specification and/or [JWA] are being used that MUST be understood and
processed. [...] If any of the listed extension Header Parameters are
not understood and supported by the recipient, then the JWS is invalid.


Proof of Concept

import jwt  # PyJWT 2.8.0
import hmac, hashlib, base64, json

# Construct token with unknown critical extension
header = {"alg": "HS256", "crit": ["x-custom-policy"], "x-custom-policy": "require-mfa"}
payload = {"sub": "attacker", "role": "admin"}

def b64url(data):
    return base64.urlsafe_b64encode(data).rstrip(b"=").decode()

h = b64url(json.dumps(header, separators=(",", ":")).encode())
p = b64url(json.dumps(payload, separators=(",", ":")).encode())
sig = b64url(hmac.new(b"secret", f"{h}.{p}".encode(), hashlib.sha256).digest())
token = f"{h}.{p}.{sig}"

# Should REJECT — x-custom-policy is not understood by PyJWT
try:
    result = jwt.decode(token, "secret", algorithms=["HS256"])
    print(f"ACCEPTED: {result}")
    # Output: ACCEPTED: {'sub': 'attacker', 'role': 'admin'}
except Exception as e:
    print(f"REJECTED: {e}")

Expected: jwt.exceptions.InvalidTokenError: Unsupported critical extension: x-custom-policy
Actual: Token accepted, payload returned.

Comparison with RFC-compliant library

# jwcrypto — correctly rejects
from jwcrypto import jwt as jw_jwt, jwk
key = jwk.JWK(kty="oct", k=b64url(b"secret"))
jw_jwt.JWT(jwt=token, key=key, algs=["HS256"])

# raises: InvalidJWSObject('Unknown critical header: "x-custom-policy"')

Impact

  • Split-brain verification in mixed-library deployments (e.g., API
    gateway using jwcrypto rejects, backend using PyJWT accepts)
  • Security policy bypass when crit carries enforcement semantics
    (MFA, token binding, scope restrictions)
  • Token binding bypass — RFC 7800 cnf (Proof-of-Possession) can be
    silently ignored
  • See CVE-2025-59420 for full impact analysis

Suggested Fix

In jwt/api_jwt.py, add validation in _validate_headers() or
decode():

_SUPPORTED_CRIT = {"b64"}  # Add extensions PyJWT actually supports

def _validate_crit(self, headers: dict) -> None:
    crit = headers.get("crit")
    if crit is None:
        return
    if not isinstance(crit, list) or len(crit) == 0:
        raise InvalidTokenError("crit must be a non-empty array")
    for ext in crit:
        if ext not in self._SUPPORTED_CRIT:
            raise InvalidTokenError(f"Unsupported critical extension: {ext}")
        if ext not in headers:
            raise InvalidTokenError(f"Critical extension {ext} not in header")

CWE

  • CWE-345: Insufficient Verification of Data Authenticity
  • CWE-863: Incorrect Authorization

References


Release Notes

jpadilla/pyjwt (pyjwt)

v2.12.0

Compare Source

Fixed


- Annotate PyJWKSet.keys for pyright by @&#8203;tamird in `#&#8203;1134 <https://github.com/jpadilla/pyjwt/pull/1134>`__
- Close ``HTTPError`` response to prevent ``ResourceWarning`` on Python 3.14 by @&#8203;veeceey in `#&#8203;1133 <https://github.com/jpadilla/pyjwt/pull/1133>`__
- Do not keep ``algorithms`` dict in PyJWK instances by @&#8203;akx in `#&#8203;1143 <https://github.com/jpadilla/pyjwt/pull/1143>`__
- Validate the crit (Critical) Header Parameter defined in RFC 7515 §4.1.11. by @&#8203;dmbs335 in `GHSA-752w-5fwx-jx9f <https://github.com/jpadilla/pyjwt/security/advisories/GHSA-752w-5fwx-jx9f>`__
- Use PyJWK algorithm when encoding without explicit algorithm in `#&#8203;1148 <https://github.com/jpadilla/pyjwt/pull/1148>`__

Added
  • Docs: Add PyJWKClient API reference and document the two-tier caching system (JWK Set cache and signing key LRU cache).

v2.11.0

Compare Source

Fixed


Added

Configuration

📅 Schedule: Branch creation - "" in timezone UTC, Automerge - At any time (no schedule defined).

🚦 Automerge: Enabled.

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

🔕 Ignore: Close this PR and you won't be reminded about these updates 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 enabled auto-merge (squash) March 14, 2026 01:52
@renovate renovate bot requested review from a team as code owners March 14, 2026 01:52
@renovate renovate bot requested a review from gjtorikian March 14, 2026 01:52
@renovate
Copy link
Contributor Author

renovate bot commented Mar 14, 2026

⚠️ Artifact update problem

Renovate failed to update an artifact related to this branch. You probably do not want to merge this PR as-is.

♻ Renovate will retry this branch, including artifacts, only when one of the following happens:

  • any of the package files in this branch needs updating, or
  • the branch becomes conflicted, or
  • you click the rebase/retry checkbox if found above, or
  • you rename this PR's title to start with "rebase!" to trigger it manually

The artifact failure details are included below:

File name: uv.lock
Command failed: uv lock --upgrade-package pyjwt --upgrade-package pyjwt
Using CPython 3.14.3 interpreter at: /opt/containerbase/tools/python/3.14.3/bin/python3
  × No solution found when resolving dependencies for split (markers:
  │ python_full_version == '3.8.*'):
  ╰─▶ Because only the following versions of pyjwt{python_full_version <
      '3.9'} are available:
          pyjwt{python_full_version < '3.9'}<=2.12.0
          pyjwt{python_full_version < '3.9'}==2.12.1
      and the requested Python version (>=3.8) does not satisfy Python>=3.9,
      we can conclude that pyjwt{python_full_version < '3.9'}>=2.12.0 cannot
      be used.
      And because your project depends on pyjwt{python_full_version <
      '3.9'}>=2.12, we can conclude that your project's requirements are
      unsatisfiable.

      hint: While the active Python version is 3.14, the resolution failed for
      other Python versions supported by your project. Consider limiting your
      project's supported Python versions using `requires-python`.

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 14, 2026

Greptile Summary

This PR attempts to remediate CVE-2026-32597 — a missing crit header validation vulnerability in PyJWT — by bumping the pyjwt dependency constraint. However, the update introduces two blocking issues and one moderate concern that prevent the security fix from actually taking effect.

Key issues found:

  • Python 3.8 constraint is broken: The new specifier >=2.12,<2.13 for python_full_version == '3.8.*' is incompatible because PyJWT dropped Python 3.8 support in 2.10.0. The original <2.10 cap existed precisely for this reason. Installing on Python 3.8 will fail with a dependency resolution error. Since the CVE fix only exists in 2.12.x (which requires Python ≥ 3.9), the project needs to decide whether to formally drop Python 3.8 support (it has been EOL since October 2024) or keep Python 3.8 pinned on the last compatible but vulnerable version.
  • uv.lock was not regenerated: The lock file still pins pyjwt 2.9.0 (for Python < 3.9) and pyjwt 2.10.1 (for Python ≥ 3.9). Any developer or CI pipeline using uv sync will install the old, vulnerable versions, making this PR a no-op from a security perspective until the lock file is committed.
  • Python 3.9+ lower bound still allows vulnerable versions: The >=2.10.0 specifier for Python 3.9+ was not tightened to >=2.12.0, leaving a window where a fresh install (without the lock file) could still resolve to a vulnerable 2.10.x or 2.11.x release.

Confidence Score: 1/5

  • This PR is not safe to merge — it will break Python 3.8 installations and does not actually apply the security fix due to a missing lock file update.
  • Two blocking defects: (1) the Python 3.8 constraint targets a PyJWT version range that explicitly dropped Python 3.8 support, causing a hard install failure for that runtime; (2) the uv.lock file was not regenerated, so the CVE-vulnerable versions (2.9.0 and 2.10.1) remain pinned and will be installed by anyone using uv sync. The security intent of the PR is sound, but it cannot take effect in its current state.
  • pyproject.toml and uv.lock both need attention — pyproject.toml has an incompatible Python 3.8 constraint, and uv.lock must be regenerated to reflect the updated specifiers.

Important Files Changed

Filename Overview
pyproject.toml Security dependency update for pyjwt — the Python 3.8 constraint is set to a version range (>=2.12,<2.13) that PyJWT does not support on Python 3.8, the uv.lock file was not regenerated so the fix is not applied, and the Python 3.9+ lower bound still allows vulnerable versions.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Install workos-python] --> B{Python version?}
    B -->|3.8.*| C["Constraint: pyjwt>=2.12,<2.13\n(from this PR)"]
    B -->|>=3.9| D["Constraint: pyjwt>=2.10.0\n(unchanged)"]

    C --> E{PyJWT 2.12 supports\nPython 3.8?}
    E -->|NO - dropped in 2.10| F["❌ Dependency resolution FAILS\nNo compatible version found"]
    E -->|YES| G["✅ Would install 2.12.0\n(CVE-2026-32597 fixed)"]

    D --> H{Lock file present?}
    H -->|YES - uv sync| I["⚠️ Installs pyjwt 2.10.1\nfrom stale uv.lock\n(still VULNERABLE)"]
    H -->|NO - fresh install| J{Resolver picks version}
    J -->|Picks >=2.12| K["✅ 2.12.0 installed\n(CVE fixed)"]
    J -->|Picks 2.10.x or 2.11.x| L["⚠️ Still VULNERABLE\n(loose lower bound >=2.10.0)"]

    style F fill:#ff4444,color:#fff
    style I fill:#ff9900,color:#fff
    style L fill:#ff9900,color:#fff
    style G fill:#44bb44,color:#fff
    style K fill:#44bb44,color:#fff
Loading

Last reviewed commit: ff82cc5

"pydantic>=2.10.4",
"pyjwt>=2.10.0 ; python_full_version >= '3.9'",
"pyjwt>=2.9.0,<2.10 ; python_full_version == '3.8.*'",
"pyjwt>=2.12,<2.13; python_full_version == '3.8.*'",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PyJWT 2.12 does not support Python 3.8

The original <2.10 upper bound was intentional: PyJWT 2.10.0 dropped Python 3.8 support (see #1007 — "Drop support for Python 3.8 (EOL)"). Changing the constraint to >=2.12,<2.13 while keeping the python_full_version == '3.8.*' marker will cause a dependency resolution failure for Python 3.8 users because no version in the [2.12, 2.13) range supports Python 3.8.

Running uv sync or pip install on Python 3.8 will fail with something like: "No solution found when resolving dependencies: Because pyjwt (>=2.12,<2.13) requires Python >=3.9 and workos depends on pyjwt>=2.12,<2.13 for Python==3.8.*, no version of workos can be installed on Python 3.8."

Since the CVE fix is only available in 2.12.x which requires Python 3.9+, the project must choose between:

  1. Dropping Python 3.8 support entirely in workos-python (Python 3.8 has been EOL since October 2024)
  2. Keeping the <2.10 cap for Python 3.8 and accepting Python 3.8 users are exposed to CVE-2026-32597

Comment on lines 14 to +15
"pyjwt>=2.10.0 ; python_full_version >= '3.9'",
"pyjwt>=2.9.0,<2.10 ; python_full_version == '3.8.*'",
"pyjwt>=2.12,<2.13; python_full_version == '3.8.*'",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

uv.lock not updated — security fix not applied

The uv.lock file was not regenerated as part of this PR. It still pins:

  • pyjwt 2.9.0 for python_full_version < '3.9'
  • pyjwt 2.10.1 for python_full_version >= '3.9'

Both versions are vulnerable to CVE-2026-32597. Anyone running uv sync (the standard install path) will still receive the old vulnerable versions, making this security fix effectively inert until the lock file is regenerated.

The [package.metadata] section in uv.lock also still reflects the old constraint >=2.9.0,<2.10 for Python 3.8.

The lock file must be regenerated (uv lock) and committed alongside this change for the security fix to actually take effect. The PR table in the description notes 2.10.1 → 2.12.0, implying both the specifier and the resolved lock version should be updated together.

@@ -12,7 +12,7 @@ dependencies = [
"httpx~=0.28.1",
"pydantic>=2.10.4",
"pyjwt>=2.10.0 ; python_full_version >= '3.9'",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Python 3.9+ lower bound still allows vulnerable versions

The Python 3.9+ specifier >=2.10.0 was not tightened to >=2.12.0. While a fresh lock file regeneration would likely resolve to 2.12.0, the loose lower bound means a user performing an unconstrained install (e.g., without the lock file, or in a mixed environment) could still receive PyJWT 2.10.x or 2.11.x — both of which are vulnerable to CVE-2026-32597.

Consider tightening this to >=2.12.0 to close this gap:

Suggested change
"pyjwt>=2.10.0 ; python_full_version >= '3.9'",
"pyjwt>=2.12.0 ; python_full_version >= '3.9'",

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

0 participants