1. Executive summary
PyJWT 2.13.0 contains an incomplete guard against JWT algorithm confusion, tracked as CVE-2026-48526 at CVSS 7.4 HIGH (CWE-287, CWE-347). The guard added in 2.13.0 to stop RSA public key material being used as an HMAC secret calls bytes.lstrip() before a startswith(b"{") JWK check; lstrip() removes only ASCII whitespace, so a UTF-8 BOM prefix (\xef\xbb\xbf) defeats the check and the public key bytes are accepted as the HMAC-SHA256 secret. An unauthenticated attacker who knows the application's public key (public by design, served from a JWKS endpoint or certificate) can forge tokens with arbitrary claims against any verifier configured with a mixed algorithm allow-list such as algorithms=["HS256", "RS256"], giving complete authentication and authorisation bypass. This is a patch bypass: organisations that upgraded to 2.13.0 specifically to remediate CVE-2026-48526 remain exposed. CVE-2026-48526 is not in CISA KEV and EPSS is 0%; the fix is in PyJWT 2.14.0, released 2026-09-11.
2. Regulatory framing
| Article | Trigger (the fact in this item) | Practical impact |
|---|---|---|
| DORA Art. 28: ICT third-party risk, general principles | The exposure sits in a third-party Python library (pyjwt on PyPI) that clients consume as a dependency, and the fix is a version bump to 2.14.0 rather than a vendor-managed patch channel |
Clients must be able to identify which in-scope applications embed PyJWT 2.13.0 and drive the upgrade through their own dependency management, not through a supplier's release cycle |
| DORA Art. 30: key contractual provisions with ICT third-party providers | Where JWT verification is delivered by an outsourced or vendor-supplied authentication component, the client's ability to require the 2.14.0 upgrade and evidence it is a contractual matter | Contract terms should give the client the right to require and verify the dependency version in a supplier-delivered authentication component |
No specific NIS2 or UK NIS 2018 article is directly engaged by this item on the facts available. There is no reported incident, no evidence of exploitation, and no supply-chain compromise of the package itself; the flaw is a code defect in a library the client deploys. If a client confirms exploitation against its own systems, DORA Art. 17 (ICT-related incident management process), Art. 18 (classification) and Art. 19 (reporting of major ICT-related incidents) become engaged at that point, and NIS2 Art. 23 (incident reporting) applies to entities in scope of that directive.
3. Technical analysis & attack chain
The affected component is PyJWT 2.13.0, the pyjwt package on PyPI. The flaw is in HMACAlgorithm.prepare_key() in jwt/algorithms.py (approximately line 344). The vulnerability class is algorithm confusion, specifically a bypass of the guard introduced in 2.13.0 to remediate CVE-2026-48526.
Confirmed attack chain:
- The attacker obtains the target application's RSA public key. This is public by design and is obtainable from a JWKS endpoint such as
/jwks.jsonor from a certificate. - The attacker prepends a UTF-8 BOM byte sequence (
\xef\xbb\xbf) to the JWK JSON representation of that public key. - The attacker signs a JWT using HS256, using the BOM-prefixed JWK bytes as the HMAC secret. The attacker does not need PyJWT to do this; any HMAC-SHA256 implementation over the same bytes produces a valid signature.
- The attacker submits the forged token to a verifier that accepts a mixed algorithm set (
algorithms=["HS256", "RS256"]or equivalent) and holds the same RSA public key as raw bytes. - PyJWT 2.13.0 accepts the token. The BOM causes the JWK detection check to be skipped, the RSA public key bytes are used as the HMAC secret, and the forged claims (
role,sub, and any others the attacker chooses) are accepted.
The mechanism is precise. The guard in 2.13.0 runs:
stripped = key_bytes.lstrip() # strips ASCII whitespace only
if stripped.startswith(b"{"): # JWK detection
...
raise InvalidKeyError("The specified key is an asymmetric key...")
bytes.lstrip() with no argument removes only bytes in the ASCII whitespace set (\x20 \t \n \r \x0b \x0c). A UTF-8 BOM prefix is not in that set, so stripped.startswith(b"{") returns False for any BOM-prefixed JWK JSON. The JWK detection block is never entered and the RSA public key bytes are silently accepted as the HMAC-SHA256 secret.
Impact is complete authentication and authorisation bypass (C:H/I:H). Attack complexity is High (AC:H) because the attacker must obtain the public key and the verifier must use a mixed-algorithm configuration; no authentication is required (PR:N). The maintainer reproduced the path on 2.13.0 and confirmed that the normal raw-JWK, asymmetric-only, and algorithm-bound PyJWK controls reject the token. The bypass is therefore conditional on an application configuration precondition, but the defect is in PyJWT's own raw-JWK validation guard and is in scope under the PyJWT security policy.
The fix is committed as 180783930de91876bc0d601f826a1f2956057291. HMACAlgorithm.prepare_key() now checks parsed JSON objects for kty across accepted UTF-8/16/32 representations, preserves non-JWK HMAC key bytes, and conservatively rejects deeply nested JSON objects even when parsing reaches the recursion guard. Regression coverage includes BOM and BOM-less encodings, deep non-JWK keys, deep JWK objects, and unpaired-surrogate cases. The full suite passes with 391 tests and 4 intentional cryptography-environment skips. A fresh Astra/max independent review accepted the commit. The fix shipped in PyJWT 2.14.0, released 2026-09-11 and available on PyPI; 2.14.0 is the first release containing the fix and is the recorded patched version.
Related advisories describe the same guard being defeated through other encodings, and clients running 2.13.0 should treat them as one remediation. GHSA-w2cx-738m-mc7w covers public RSA, EC, or OKP JWKs accepted as HMAC secrets when wrapped in a JWKS object, nested in an array, or otherwise not exposing a top-level kty member; the fix is on master in commit 801cd12 and is included in 2.14.0. GHSA-ffc3-869f-jxw9 covers PEM public keys with marker-adjacent whitespace, CR-only line terminators, or folded to a single line, where is_pem_format() returns False while cryptography.load_pem_public_key() accepts the identical bytes; this is an incomplete-guard bypass of the CVE-2022-29217 family. GHSA-p4g4-x82p-q773 covers DER-encoded public keys (SubjectPublicKeyInfo, PKCS#1, and X.509 certificates), which start with 0x30 0x82 and match neither the -----BEGIN nor the ssh- text markers the guard searches for; the advisory states this affects 2.4.0, 2.13.0, and main at commit 7144e4534, and that applications using PyJWK or PyJWKClient are not affected because jwt/api_jws.py:395 binds the header alg to the key's own algorithm.
Confidence caveats. The BOM bypass mechanism, the affected version, the fix commit, and the 2.14.0 release are corroborated by the primary advisory and the maintainer updates. The DER and PEM-encoding variants are single-sourced to their respective GitHub advisories and have not been independently confirmed here; verify against your own PyJWT version before treating them as equivalent exposure. No exploitation in the wild is reported in any source, and CVE-2026-48526 is not in CISA KEV.
4. Mitigation & containment
P1, within 24 hours:
- Inventory every application and service that embeds
pyjwt. Runpip show pyjwtandpip freeze | grep -i pyjwtacross build images and deployed environments, and check lockfiles (requirements.txt,poetry.lock,Pipfile.lock,uv.lock) for a pinned2.13.0. The affected range is exactly= 2.13.0. - Upgrade to PyJWT 2.14.0, the first release containing the fix. Where an immediate upgrade is blocked, pin below 2.13.0 rather than staying on 2.13.0, and treat the pin as temporary.
- Find and fix mixed algorithm allow-lists. Search for
algorithms=injwt.decodecall sites and remove any list that contains both an HMAC algorithm (HS256,HS384,HS512) and an asymmetric one (RS256,ES256, and so on). An asymmetric-only allow-list is not exploitable through this path. This is the configuration precondition the maintainer identified, and it is the fastest control available while upgrades are staged. - Move verification onto
PyJWKorPyJWKClient. The maintainer states that the algorithm-boundPyJWKpath rejects the forged token, so this removes the exposure without waiting for a release.
P2, within 72 hours:
- Rotate any JWT signing or verification secrets that were configured as raw public key material, and audit token issuance logs for tokens whose
algheader isHS256but which were validated against an asymmetric key. - Review authentication logs for successful logins or privileged actions whose tokens carry claims inconsistent with the account's normal role or
sub. The forged token carries attacker-chosen claims, so the observable is an authorisation decision that does not match the identity. - If you cannot confirm the algorithm allow-list for a given service, treat that service as exposed and prioritise it.
P3, within 7 days:
- Add a dependency policy that rejects mixed symmetric/asymmetric JWT algorithm allow-lists at code review, and add a CI check that fails a build pinning
pyjwt==2.13.0. - Track the related advisories (GHSA-w2cx-738m-mc7w, GHSA-ffc3-869f-jxw9, GHSA-p4g4-x82p-q773) against the same 2.14.0 upgrade. The DER variant in GHSA-p4g4-x82p-q773 is reported to affect versions from 2.4.0 onward, so a client that cannot reach 2.14.0 should not assume an older pin is safe on the DER path.
5. Indicators of compromise
No indicators of compromise available in the source material. The sources describe a code defect and a proof-of-concept, not observed malicious activity. There are no hashes, domains, IP addresses, or file paths associated with an intrusion.
The sources do describe observable behaviours that a defender can hunt for. These are configuration and authentication patterns, not atomic indicators, and they do not belong in a machine-pivotable block.
Behavioural indicators:
| behaviour | where to observe | confidence |
|---|---|---|
jwt.decode called with an allow-list mixing an HMAC algorithm and an asymmetric algorithm, e.g. algorithms=["HS256", "RS256"] |
application source, static analysis of jwt.decode call sites |
high, stated by the maintainer as the precondition |
Verification key passed as raw public JWK/JWKS JSON or raw PEM/DER bytes on the non-PyJWK path rather than through PyJWK/PyJWKClient |
application source, jwt/api_jws.py call path |
high for the JWK path, single-sourced for the PEM and DER paths |
A token with alg: HS256 accepted by a service whose verification key is an RSA or EC public key |
authentication service logs, JWT header inspection at the gateway | high, this is the signature of a successful forgery |
Successful authorisation for claims (role, sub) that do not match the account's normal profile |
application authorisation logs, SIEM correlation on identity | medium, requires a baseline of normal claim values |
6. Detection
The sources contain no file hashes, mutexes, command-line flags, or network artefacts. They do contain distinctive code-level artefacts: the guard function, the BOM byte sequence, and the specific API call patterns. A YARA rule over source or bytecode is possible using the BOM sequence and the guard's distinctive string, but the BOM sequence alone is a two-to-three byte pattern that will match enormous volumes of legitimate UTF-8 files, and the guard string is a fragment of the vulnerable library rather than of an attacker's tooling. Neither meets the bar for a threat artefact. A Sigma rule is the better fit here, because the observable is a configuration and authentication pattern rather than a file.
title: PyJWT Mixed Algorithm Allow-List With HS256 Token Against Asymmetric Key
id: 8f3a1c2e-4b7d-4e91-9a2c-6d5f0e8b1a34
status: experimental
description: >
Detects JWT verification where the token header algorithm is HS256 but the
verifier is configured with an asymmetric key, the configuration precondition
for the PyJWT 2.13.0 BOM bypass (CVE-2026-48526). Requires the application to
emit the verification key type and the token alg header to logs.
author: Adverse Trace
date: 2026-09-30
references:
- https://github.com/advisories/GHSA-r6x4-923q-g947
logsource:
product: application
service: authentication
detection:
selection_alg:
jwt.header.alg: 'HS256'
selection_keytype:
jwt.verification.key_type:
- 'RSA'
- 'EC'
- 'OKP'
condition: selection_alg and selection_keytype
falsepositives:
- Applications that legitimately verify HS256 tokens with a symmetric secret
and separately log an asymmetric key for a different verification path.
- Logging that does not distinguish which key was used for which verification.
level: high
This rule depends on the application logging both the token alg header and the verification key type. Where that telemetry does not exist, the equivalent control is a static analysis check on jwt.decode call sites for mixed algorithm allow-lists, which is a code review task rather than a detection rule.
CVE assessment
1 referenced CVE
| CVE | CVSS | Exploited | EPSS | Summary |
|---|---|---|---|---|
| CVE-2026-48526 | 7.4 High | — | 0% | PyJWT is a JSON Web Token implementation in Python. Prior to 2.13.0, when the verifier is decoding JSON Web Tokens, while suppo… |
7. Sources
- GitHub Security Advisories, PyJWT BOM Bypass (GHSA-r6x4-923q-g947), https://github.com/advisories/GHSA-r6x4-923q-g947, published 2026-09-29.
- GitHub Security Advisories, PyJWT accepts public JWK containers as HMAC secrets (GHSA-w2cx-738m-mc7w), https://github.com/advisories/GHSA-w2cx-738m-mc7w.
- GitHub Security Advisories, PyJWT: Asymmetric-PEM detection bypass (GHSA-ffc3-869f-jxw9), https://github.com/advisories/GHSA-ffc3-869f-jxw9.
- GitHub Security Advisories, PyJWT: Public keys in DER form are accepted as HMAC secrets (GHSA-p4g4-x82p-q773), https://github.com/advisories/GHSA-p4g4-x82p-q773.
- GitHub Advisory Database, CVE-2026-102272 (GHSA-r6x4-923q-g947), https://github.com/advisories/GHSA-r6x4-923q-g947.
8. Adverse Trace position
Adverse Trace assesses CVE-2026-48526 at CVSS 7.4 HIGH, not in CISA KEV, EPSS 0%. The score is accurate for the flaw as described: the impact is complete authentication and authorisation bypass, but the precondition (a mixed algorithm allow-list plus a raw public key passed on the non-PyJWK path) narrows the affected population, and no exploitation is reported. The EPSS 0% and the absence of KEV listing mean this is not a patch-today emergency for most clients, but the patch-bypass property changes the calculus for anyone who upgraded to 2.13.0 specifically to close CVE-2026-48526: that upgrade did not achieve its objective, and the exposure is unchanged. For EMEA financial services clients, the practical risk is concentrated in authentication services that verify JWTs with a public key and also accept HS256, which is a common pattern in API gateways and internal service meshes. We will track the PyJWT 2.14.0 release and any downstream packages that pin 2.13.0, and we will update this advisory if exploitation is reported or if the DER and PEM-encoding variants are independently confirmed. Clients should confirm their jwt.decode algorithm allow-lists and their PyJWT version this week; the two checks together take minutes and close the exposure without waiting for a release cycle.
Published via PulseTrace — Adverse Trace threat intelligence.