GitHub Advisoryadvisory · PyPI
GHSA-7vr2-2fgp-9w7p Authlib incomplete algorithm-confusion fix permits HMAC forgery with prefixed public keys
HIGH
Official page ↗
- Aliases
- CVE-2026-114615
- Ecosystem
- PyPI
- Packages
- authlib
- Severity
- High
- CVSS vector
- CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N
- Weaknesses
- CWE-184, CWE-347
- Reviewed
- 2026-10-27
Affected packages
• PyPI authlib: >= 1.3.1, < 1.9.0 → fixed in 1.9.0
Description
Summary
`OctKey.import_key()` carries the only guard that stops an asymmetric public key from being used as an
HMAC secret in `authlib.jose`. That guard is `raw_key.startswith(POSSIBLE_UNSAFE_KEYS)` — anchored at
byte 0 — while the loader that consumes the identical bytes on the asymmetric path
(`cryptography.hazmat.primitives.serialization.load_pem_public_key` / `load_pem_x509_certificate`)
skips anything before the `-----BEGIN` line. A PEM public key or X.509 certificate carrying one
leading byte — a newline, a space, four spaces of indentation, a UTF-8 BOM, or a comment line — is
therefore not recognised, falls through, and becomes the HMAC secret. An attacker holding only the
issuer's public key then mints an `HS256` token with arbitrary claims that `jwt.decode()` accepts as
authentic.
This is an incomplete fix of CVE-2024-37568 / GHSA-5357-c2jx-v7qh. That advisory's fix is exactly
this guard (`git log -S 'POSSIBLE_UNSAFE_KEYS' -- authlib/jose/rfc7518/oct_key.py` returns a single
commit, `3bea812` "fix: prevent OctKey to import ssh/rsa/pem keys", referencing issue #654), and the
original attack works again unchanged whenever the stored PEM does not start at offset 0.
Reproduced end-to-end on 1.8.0, the newest release (published 2026-08-30), and on `origin/main`
(`c529a61`), with `cryptography` 50.0.2.
Preconditions
Both are deployment properties; neither is attacker-controlled at request time, and the attacker needs
no secret and no user interaction.
1. The relying party uses the documented module-level `jwt` object. `from authlib.jose import jwt`
is built at `authlib/jose/__init__.py:55-72` with the full list
`["HS256","HS384","HS512","RS256",...,"EdDSA"]`, and `JsonWebToken.decode`
(`authlib/jose/rfc7519/jwt.py:80`) exposes no `algorithms` parameter, so a caller of the
documented object has no way to narrow it. This is the first code sample in
`docs/jose/jwt.rst:21-31`.
This hazard is already documented: `docs/jose/jwt.rst:73-79` says
"This decoding method is insecure. By default `jwt.decode` parses the alg header. This allows
symmetric macs and asymmetric signatures.", and `docs/jose/jwt.rst:94-98` says "You should never
combine symmetric (HS) and asymmetric (RS, ES, PS) signature schemes." The point of this report is
not that the mixed list exists — it is that the `oct_key.py:81` backstop shipped for precisely
that documented hazard does not hold.
2. The verification key is passed as PEM/certificate text whose bytes do not begin at offset 0 with
`-----BEGIN `. Each of the following forms was confirmed to forge, and each also still verifies a
genuine RS256/ES256 token with the identical bytes, so nothing in the deployment looks broken:
• one leading newline — what a triple-quoted Python literal produces
• one leading space
• four-space indentation — an indented YAML/JSON/TOML config block
• a UTF-8 BOM — a Windows-editor artifact
• a leading comment line
• all of the above applied to an X.509 `CERTIFICATE` PEM instead of a `PUBLIC KEY` PEM
A raw DER SPKI blob also bypasses the guard, but DER cannot be used for genuine asymmetric
verification through this path, so I exclude it as a non-deployable configuration and mention it
only as further evidence of the recogniser/loader gap.
Details
`authlib/jose/rfc7518/oct_key.py:10-17`
```python
POSSIBLE_UNSAFE_KEYS = (
b"-----BEGIN ",
b"---- BEGIN ",
b"ssh-rsa ",
b"ssh-dss ",
b"ssh-ed25519 ",
b"ecdsa-sha2-",
)
```
`authlib/jose/rfc7518/oct_key.py:78-84`
```python
raw_key = to_bytes(raw)
security check
if raw_key.startswith(POSSIBLE_UNSAFE_KEYS):
raise ValueError("This key may not be safe to import")
key = cls(raw_key=raw_key, options=options)
```
`bytes.startswith` is anchored. The loader on the asymmetric path is not:
`authlib/jose/rfc7517/_cryptography_key.py:22-29` calls `load_pem_public_key` /
`load_pem_x509_certificate`, both of which scan forward for the armour header. So the set of byte
strings the recogniser rejects is strictly smaller than the set the loader accepts, and the difference
is exploitable.
Reach, for a caller of the documented object:
• `authlib/jose/rfc7518/jws_algs.py:59` — `HMACAlgorithm.prepare_key(raw_data)` returns
`OctKey.import_key(raw_data)`; this is the whole of the HMAC key handling.
• `authlib/jose/rfc7519/jwt.py:80` — `JsonWebToken.decode(self, s, key, claims_cls=None,
claims_options=None, claims_params=None)`; the algorithm comes from the token header and is checked
only against the instance's list.
• `authlib/jose/__init__.py:55-72` — that list contains both HS\* and RS\*/ES\*/PS\*/EdDSA.
So `oct_key.py:81` is the only thing between a mixed allow-list and using the public key as an HMAC
secret, and it is bypassed by a single leading byte.
PoC
`poc-authlib-e2e-http.py` stands up a Flask API whose only authentication is the documented
`jwt.decode(token, PUBLIC_KEY)`, where `PUBLIC_KEY` is the issuer's RSA public key embedded as a
triple-quoted Python literal (hence one leading newline). The attacker is given only that public
key.
```
authlib=1.8.0 cryptography=50.0.2 flask=3.1.3
guard = authlib/jose/rfc7518/oct_key.py:81 raw_key.startswith(POSSIBLE_UNSAFE_KEYS)
call = authlib/jose/rfc7519/jwt.py:80 JsonWebToken.decode(s, key) [no `algorithms` param]
algs = authlib/jose/__init__.py:55+ module-level jwt = JsonWebToken([HS256..RS256..ES256..])
[baseline 1] genuine RS256 token, role=user -> (403, '{"error":"not admin","sub":"alice"}')
[baseline 2] genuine RS256 token, role=admin -> (200, '{"data":"DB_ROOT_PASSWORD=hunter2; STRIPE_SK=sk_live_deadbeef","ok":true,"sub":"root"}')
[CONTROL 1 ] payload tampered, RS256 sig kept -> (401, '{"error":"BadSignatureError: bad_signature: "}')
[CONTROL 2 ] HS256 forged with CANONICAL pubkey -> BLOCKED at key import: ValueError: This key may not be safe to import
[CONTROL 3 ] JsonWebToken(['RS256']) vs forged token -> REJECTED: UnsupportedAlgorithmError
ATTACKER INPUT: only IDP_PUB_CANONICAL (public by definition), no private key.
forged token header: {'alg': 'HS256', 'typ': 'JWT'}
[ATTACK ] HS256 forged with the PUBLIC key -> (200, '{"data":"DB_ROOT_PASSWORD=hunter2; STRIPE_SK=sk_live_deadbeef","ok":true,"sub":"attacker"}')
```
The three controls establish that the guard does work on a canonical PEM (so the leading byte is
load-bearing), that ordinary signature verification is intact, and that pinning the algorithm is an
effective mitigation.
Per-form matrix, both key types, with a control proving genuine asymmetric verification still succeeds
on the exact same bytes:
```
RSA-2048/RS256
canonical PEM (CONTROL A) loader=load_pem_public_key startswith_guard=True RS/ES-verify=OK BLOCKED:ValueError
indented PEM (4 spaces) loader=load_pem_public_key startswith_guard=False RS/ES-verify=OK FORGED sub=superadmin role=admin
UTF-8 BOM prefix loader=load_pem_public_key startswith_guard=False RS/ES-verify=OK FORGED sub=superadmin role=admin
leading comment line loader=load_pem_public_key startswith_guard=False RS/ES-verify=OK FORGED sub=superadmin role=admin
leading blank line loader=load_pem_public_key startswith_guard=False RS/ES-verify=OK FORGED sub=superadmin role=admin
leading single space loader=load_pem_public_key startswith_guard=False RS/ES-verify=OK FORGED sub=superadmin role=admin
DER (SPKI) bytes loader=load_der_public_key startswith_guard=False RS/ES-verify=FAIL FORGED sub=superadmin role=admin
[CONTROL C] JsonWebToken(['RS256']).decode(forged HS256) -> REJECTED:UnsupportedAlgorithmError
EC-P256/ES256
identical results for all seven rows
Same five leading-byte forms applied to an X.509 CERTIFICATE PEM: also FORGED, RS256-verify=OK.
```
Steps to reproduce
1. `pip install authlib==1.8.0 cryptography flask requ…
Related
Alias · CVE (1)
- CVE-2026-114615 Authlib is a Python library which builds OAuth and OpenID Connect servers. vulnerability
References
- https://github.com/authlib/authlib/security/advisories/GHSA-7vr2-2fgp-9w7p web
- https://nvd.nist.gov/vuln/detail/CVE-2026-114615 advisory
- https://github.com/authlib/authlib/pull/945 web
- https://github.com/authlib/authlib/commit/e3bd41cb29dcfee434453b135ffb487884baeedf web
- https://github.com/authlib/authlib package
- https://github.com/authlib/authlib/releases/tag/v1.9.0 web