> ## Content Index
> Fetch the complete content index at: https://f4n6.co.uk/llms.txt
> Use this file to discover other available public pages before exploring further.

# PyJWT BOM Bypass
- URL: https://f4n6.co.uk/security-feed/pyjwt-bom-bypass/
- Published: 2026-09-29T23:51:05.000Z
- Updated: 2026-09-29T23:51:05.000Z
- Author: Jeff Davies
- Tags: #security-feed, CVE-2026-48526

## 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:

1. 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.json` or from a certificate.
2. The attacker prepends a UTF-8 BOM byte sequence (`\xef\xbb\xbf`) to the JWK JSON representation of that public key.
3. 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.
4. 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.
5. 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:

```python
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`. Run `pip show pyjwt` and `pip freeze | grep -i pyjwt` across build images and deployed environments, and check lockfiles (`requirements.txt`, `poetry.lock`, `Pipfile.lock`, `uv.lock`) for a pinned `2.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=` in `jwt.decode` call 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 `PyJWK` or `PyJWKClient`. The maintainer states that the algorithm-bound `PyJWK` path 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 `alg` header is `HS256` but 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.

```yaml
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](https://nvd.nist.gov/vuln/detail/CVE-2026-48526?ref=f4n6.co.uk) | 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.

---

[Read the original source →](https://github.com/advisories/GHSA-r6x4-923q-g947?ref=f4n6.co.uk)

*Published via PulseTrace — Adverse Trace threat intelligence.*