Overview
PyJWT is the most widely used Python implementation of the JSON Web Token (JWT) standard, embedded in countless authentication and API-authorization flows. A critical vulnerability tracked as CVE-2026-102268 (CVSS 9.1) has been disclosed in versions prior to 2.14.0, centered on the library's is_pem_format helper in jwt/utils.py.
The flaw is a PEM-format detection weakness: is_pem_format does not recognize every PEM representation that the underlying cryptography library's loader will happily accept. In deployments that verify tokens against an allow-list mixing HMAC (HS256) and asymmetric (e.g. RS256/ES256) algorithms, this gap can let a public key be silently reinterpreted as an HMAC secret — the classic algorithm-confusion primitive that has plagued JWT libraries for years. When that happens, anyone who knows the (public, by definition) verification key can forge tokens that pass signature verification.
This matters because algorithm-confusion bugs in JWT tooling are not theoretical curiosities — they translate directly into authentication bypass and full token forgery, often across every service that trusts the affected verifier.
Technical Details
| Field | Value |
|---|---|
| CVE ID | CVE-2026-102268 |
| Severity | Critical (CVSS 9.1) |
| CWE | CWE-347 (Improper Verification of Cryptographic Signature) |
| Component | jwt/utils.py — is_pem_format(), consumed by the guard in jwt/algorithms.py (HMACAlgorithm.prepare_key) |
| Affected Versions | PyJWT versions prior to 2.14.0 |
| Fixed Version | PyJWT 2.14.0 (released 2026-09-11) |
| Impact | JWT signature/algorithm-confusion bypass — forged HMAC-signed tokens accepted as valid |
How It Works
PyJWT already carries a guard, added for an earlier issue (CVE-2022-29217), that is supposed to reject an asymmetric key when it's handed to an HMAC algorithm. That guard only fires when is_pem_format() recognizes the input as PEM data — and that recognizer is stricter than it needs to be.
is_pem_format() matches against a regular expression that expects a fairly rigid structure: a BEGIN marker followed by an LF-terminated line, body content, and a matching END marker, with only spaces and hyphens tolerated immediately around the markers. The cryptography library's load_pem_public_key(), which actually parses the key material, is considerably more permissive — it accepts:
- Extra whitespace (tabs or spaces) directly adjacent to the
BEGIN/ENDmarkers - Bare carriage-return (
\r-only) line terminators instead of the\r?\nthe regex requires - PEM content folded onto a single line
Any of these forms can arise organically — a PEM value indented inside a YAML or JSON config block, a key passed through a single-line environment variable, or a file that has been through a CR/LF round-trip in some tooling. When a key in one of these forms is used, is_pem_format() returns False, so the asymmetric-key guard never engages, even though the exact same bytes load successfully as a valid public key elsewhere in the pipeline. HMACAlgorithm.prepare_key then treats those raw bytes as an HMAC secret. If the application's verification call allows both an HMAC and an asymmetric algorithm (e.g. algorithms=["HS256", "ES256"]), an attacker who has the (public) verification key can compute a valid HS256 signature over a token of their choosing, and PyJWT will accept it as authentic.
Impact Assessment
Who Is At Risk
Any application that:
- Uses PyJWT to verify incoming JWTs, and
- Configures its
algorithms=[...]allow-list to accept both an HMAC algorithm (HS256/HS384/HS512) and an asymmetric algorithm (RS256,ES256, etc.) on the same verification path, and - Has not yet upgraded to PyJWT 2.14.0 or later
is potentially exposed, regardless of whether the application itself ever intended to issue HMAC-signed tokens.
Potential Attack Chains
- Key acquisition — Attacker obtains the application's public verification key (by definition public — often published in a JWKS endpoint, embedded in client code, or otherwise not secret).
- PEM mutation — Attacker (or, more realistically, the normal handling pipeline) supplies that key in one of the loader-accepted-but-regex-missed forms (extra whitespace near markers, CR-only line endings, single-line folding).
- Guard bypass —
is_pem_format()fails to flag the key as PEM, so PyJWT's asymmetric-key rejection never triggers. - Algorithm confusion —
HMACAlgorithm.prepare_keytreats the public key bytes as an HMAC secret. - Token forgery — Attacker signs an arbitrary JWT payload with HS256 using the public key as the "secret," and the forged token passes verification as if it were legitimately issued.
Mitigation
Immediate Actions
- Upgrade to PyJWT 2.14.0 or later, which hardens PEM/key-format detection so it matches every form the
cryptographyloader accepts. - Explicitly pin the algorithms parameter on every
jwt.decode()call — passalgorithms=[...]with a single, specific algorithm family rather than a permissive list. - Never mix HMAC and asymmetric algorithms in the same verification allow-list. If different token issuers require different algorithms, verify them through separate, algorithm-specific code paths rather than a shared allow-list.
Detection Opportunities
- Audit codebases for
jwt.decode(...)calls that pass analgorithmslist containing both anHS*entry and anRS*/ES*/PS*entry. - Review any configuration, secrets-management, or environment-variable pipeline that stores PEM keys, since CR/LF normalization or line-folding during storage/retrieval is exactly the kind of mutation that triggers this bug.
- Monitor authentication logs for unexpected
HS256-signed tokens where the service otherwise expects asymmetric-signed tokens.
Defence-in-Depth
- Treat JWT verification configuration as security-critical code — require review for any change to
algorithms=[...]lists. - Where possible, use distinct key material and distinct verification functions for HMAC-based and asymmetric-based token flows so a confusion bug in one library can't cross algorithm boundaries.
- Keep dependency-scanning/SCA tooling current so PyJWT and other cryptographic libraries are flagged promptly when new advisories land.
Background
Algorithm-confusion attacks are a well-established vulnerability class in JWT implementations, not a novelty specific to PyJWT. The core pattern — a verifier configured to accept both symmetric and asymmetric algorithms, allowing an attacker who knows a public key to forge an HMAC-signed token — has surfaced repeatedly across the JWT ecosystem over the years, which is why PyJWT already carried a guard against it (originally added for CVE-2022-29217). CVE-2026-102268 shows how narrowly-scoped format-detection logic can reintroduce a previously-fixed class of bug: the guard itself was sound, but the PEM recognizer feeding it was stricter than the actual key parser, leaving a gap that mutated-but-valid PEM input could slip through. PyJWT 2.14.0 addressed this alongside a cluster of related advisories covering JWK, JWKS, array, encoded, BOM-prefixed, and DER-form key inputs to the same HMAC guard.