CVE-2026-102268: PyJWT Asymmetric-PEM Detection Bypass: Whitespace/line-ending-mutated Public Vulnerability
CRITICALCVE-2026-102268 is a CRITICAL (CVSS 9.1) asymmetric-pem detection bypass: whitespace/line-ending-mutated public vulnerability (CWE-347: Improper Verification of Cryptographic Signature) affecting PyJWT libraries in the PyPI ecosystem. The issue involves asymmetric-pem detection bypass: whitespace/line-ending-mutated public keys skip the hs/asymmetric confusion guard. Remediation is available by upgrading affected packages to version 2.14.0.
CVE-2026-102268 at a Glance
Security Impact
CVSS: 9.1
The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N).
How to Fix CVE-2026-102268
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.
pip install PyJWT>=2.14.0
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-102268, 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.14.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
CVE-2026-102268 Affected PyJWT Versions
1 component(s)| Product / Package | Ecosystem | Affected Versions | Fixed Version |
|---|---|---|---|
|
PyJWT
|
PyPI |
< 2.14.0
|
2.14.0
|
References
Frequently Asked Questions
What is CVE-2026-102268?
Which PyJWT versions are affected?
What version fixes CVE-2026-102268?
Is PyJWT affected?
What is the CVSS score of CVE-2026-102268?
What CWE is associated with CVE-2026-102268?
How can I check PyPI dependencies for CVE-2026-102268?
Machine-readable OSV JSON
OSV v1.5 Schema
{
"schema_version" : "1.4.0",
"id" : "GHSA-ffc3-869f-jxw9",
"modified" : "2026-09-29T23:17:34Z",
"published" : "2026-09-29T23:17:33Z",
"aliases" : [ "CVE-2026-102268" ],
"summary" : "PyJWT: Asymmetric-PEM detection bypass: whitespace/line-ending-mutated public keys skip the HS/asymmetric confusion guard",
"details" : "**Prerequisites** (both conditions must hold; both are deployment properties, not attacker-controlled at request time):\n\n- The `jwt.decode` allow-list mixes an HMAC algorithm with an asymmetric one, e.g. `algorithms=[\"ES256\", \"HS256\"]` (the RFC 8725 footgun the guard exists to backstop).\n- The verification key is passed as raw PEM text/bytes on the non-`PyJWK` path, in a byte-form that `cryptography`'s loader accepts but PyJWT's `is_pem_format` regex does not recognize (marker-adjacent whitespace/indentation, CR-only line terminators, or the PEM folded to a single line). Such forms arise naturally from an indented YAML/JSON block, a single-line environment variable or JSON string, or a CR/LF round-trip through config tooling.\n\nThe attacker additionally needs the public verification key, which is public by definition, and `cryptography` must be installed.\n\nThe fix for CVE-2022-29217 rejects an asymmetric key handed to an HMAC algorithm, but only when `is_pem_format()` recognizes the key as PEM. That recognizer accepts strictly fewer byte-forms than the loader that later parses the key, so a PEM the guard misses still loads as a valid public key and is then used as an HMAC secret. This is an incomplete-guard bypass of the CVE-2022-29217 family.\n\n### Summary\nA PEM public key with marker-adjacent whitespace, CR-only line terminators, or folded to a single line makes PyJWT's `is_pem_format()` return `False` while `cryptography.load_pem_public_key()` accepts the identical bytes. The asymmetric-key rejection in `HMACAlgorithm.prepare_key` is skipped, the public key becomes the HMAC secret, and an attacker who knows the public key mints a valid `HS256` token — universal forgery — whenever the verify allow-list mixes an HMAC and an asymmetric algorithm.\n\n### Details\nAt `jwt/algorithms.py:331-335`, `HMACAlgorithm.prepare_key` contains the sole family-mismatch guard:\n\n if is_pem_format(key_bytes) or is_ssh_key(key_bytes):\n raise InvalidKeyError(\n \"The specified key is an asymmetric key or x509 certificate and\"\n \" should not be used as an HMAC secret.\"\n )\n\nIf neither predicate fires, `:357` returns `key_bytes` unchanged — the PEM text is used directly as the HMAC secret.\n\n`is_pem_format` (`jwt/utils.py:116-127`) is `bool(_PEM_RE.search(key))`, where `_PEM_RE` requires `----[- ]BEGIN (...)[- ]----\\r?\\n`, then `.+?\\r?\\n`, then the END marker. The LF in each `\\r?\\n` is mandatory, the markers are anchored directly after a newline, and only `[- ]` is tolerated adjacent to them — not arbitrary whitespace. So a key with a tab/space before the END marker, with bare `\\r` terminators, or folded onto one line is not recognized as PEM. `cryptography`'s `load_pem_public_key` is tolerant of exactly these forms and still returns the key.\n\nReach: `jwt/api_jws.py:386` performs the allow-list check (passes when `HS256` is in the list) and takes the non-`PyJWK` branch to `alg_obj.prepare_key(key)` at `:407`. The mismatch guard above is the only thing standing between a mixed allow-list and using the public key as an HMAC secret.\n\n### PoC\nVulnerable path: `jwt/algorithms.py:331` (guard gated on `is_pem_format`) -> `is_pem_format` returns `False` for a loader-accepted PEM -> `jwt/algorithms.py:357` returns the public-key bytes as the HMAC secret -> `HS256` verification succeeds.\n\nReproduced on PyJWT 2.13.0 (commit `7144e453`) with `cryptography` 49.0.0, using only the public API. For each of an EC (ES256) and an RSA-2048 (RS256) key: start from the correct public-key PEM, apply a mutation, confirm `is_pem_format` now returns `False` while `cryptography` still loads the bytes, then verify a token signed `alg=HS256` with the public-key text as the HMAC secret, under `algorithms=[\"ES256\",\"HS256\"]` (resp. `[\"RS256\",\"HS256\"]`).\n\nObserved output:\n\n pyjwt 2.13.0\n ec canonical (control) is_pem_format=True crypto_loads=True forgery=BLOCKED:InvalidKeyError\n ec marker-adjacent (tab before END) is_pem_format=False crypto_loads=True forgery=FORGED(superadmin)\n ec CR-only terminators is_pem_format=False crypto_loads=True forgery=FORGED(superadmin)\n ec folded single-line is_pem_format=False crypto_loads=True forgery=FORGED(superadmin)\n rsa canonical (control) is_pem_format=True crypto_loads=True forgery=BLOCKED:InvalidKeyError\n rsa marker-adjacent (tab before END) is_pem_format=False crypto_loads=True forgery=FORGED(superadmin)\n rsa CR-only terminators is_pem_format=False crypto_loads=True forgery=FORGED(superadmin)\n rsa folded single-line is_pem_format=False crypto_loads=True forgery=FORGED(superadmin)\n [control B] single-alg [ES256] allow-list vs forged HS256: REJECTED:InvalidAlgorithmError\n RESULT: ALL-INVARIANTS-HOLD\n\nEach mutated form on both key types forged a token accepted as `superadmin`. Controls: the unmodified PEM is correctly rejected with `InvalidKeyError` (the guard works and the mutation is load-bearing); a single-algorithm allow-list `[\"ES256\"]` rejects the forged `HS256` token with `InvalidAlgorithmError` (the mixed allow-list is a necessary precondition).\n\nSteps to reproduce:\n1. Generate an EC P-256 (or RSA-2048) keypair; serialize the public key to PEM.\n2. Mutate the PEM into a loader-accepted, regex-missed form — e.g. insert a tab before `-----END`, convert terminators to bare `\\r`, or join all lines into one.\n3. Confirm `jwt.utils.is_pem_format(mutated) is False` and `cryptography.hazmat.primitives.serialization.load_pem_public_key(mutated)` succeeds.\n4. `jwt.encode({\"sub\":\"superadmin\"}, mutated, algorithm=\"HS256\")`, then `jwt.decode(token, mutated, algorithms=[\"ES256\",\"HS256\"])` — verification succeeds.\n\n### Impact\nCryptographic signature-verification bypass (CWE-347): algorithm confusion re-enabled by an incomplete asymmetric-key guard. An attacker who knows only the public verification key forges arbitrary-claim tokens that verify as authentic, subject to the two deployment preconditions above. The `PyJWK` verification path binds a single algorithm and is unaffected; `enforce_minimum_key_length` (off by default) does not block a 2048-bit/P-256 PEM. Impact when the preconditions hold is critical (universal forgery); the compound precondition is realistic but was not observed in a specific real-world deployment, so this is rated Critical, with the deployment precondition captured in CVSS.\n\n## Maintainer update — 2026-09-10\n\nWe reproduced the reported asymmetric-key guard bypass on PyJWT 2.13.0: PEM public keys with loader-accepted formatting mutations were missed by `is_pem_format`, then accepted as HMAC secrets when a verification call mixed symmetric and asymmetric algorithms. Canonical PEM controls remained blocked, and a single-algorithm allow-list rejected the forged HS256 token.\n\nThe fix is committed as `8b4e233a22206b34ec1186e912e75c0b2396ac07`. PyJWT now scans supported PEM BEGIN/END markers in one pass, preserving matching labels and handling overlapping markers without regex backtracking. Regression coverage includes RSA loader-accepted mutations, incomplete repeated markers, later valid PEM blocks, and overlapping END/BEGIN markers. The fix does not broaden DER-key classification or change the caller's algorithm allow-list policy.\n\nVerification on the signed commit passes with 400 tests and 4 intentional cryptography-environment skips; Ruff formatting/lint and the Python 3.9 mypy tox target pass. Fresh Astra/max independent review accepted the final snapshot and confirmed O(n) scanning, bounded storage, and no blocking compatibility or security finding. The fix has not been released; the advisory remains Critical with CVSS 3.1 score 9.1 and CWE-347.\n\n## Maintainer update — 2026-09-11\n\nThe 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.",
"severity" : [ {
"type" : "CVSS_V3",
"score" : "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N"
} ],
"affected" : [ {
"package" : {
"ecosystem" : "PyPI",
"name" : "PyJWT"
},
"ranges" : [ {
"type" : "ECOSYSTEM",
"events" : [ {
"introduced" : "0"
}, {
"fixed" : "2.14.0"
} ]
} ],
"database_specific" : {
"last_known_affected_version_range" : "<= 2.13.0"
}
} ],
"references" : [ {
"type" : "WEB",
"url" : "https://github.com/jpadilla/pyjwt/security/advisories/GHSA-ffc3-869f-jxw9"
}, {
"type" : "ADVISORY",
"url" : "https://nvd.nist.gov/vuln/detail/CVE-2026-102268"
}, {
"type" : "WEB",
"url" : "https://github.com/jpadilla/pyjwt/commit/8b4e233a22206b34ec1186e912e75c0b2396ac07"
}, {
"type" : "PACKAGE",
"url" : "https://github.com/jpadilla/pyjwt"
}, {
"type" : "WEB",
"url" : "https://github.com/jpadilla/pyjwt/releases/tag/2.14.0"
} ],
"database_specific" : {
"cwe_ids" : [ "CWE-347" ],
"severity" : "CRITICAL",
"github_reviewed" : true,
"github_reviewed_at" : "2026-09-29T23:17:33Z",
"nvd_published_at" : "2026-09-28T21:17:14Z"
}
}