
CVE-2026-19553を再現するProof-of-Conceptラボ。CPythonの`ssl.SSLContext.wrap_bio()`が`server_hostname`がNoneの場合にTLSホスト名検証をサイレントにスキップする問題。
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · cve-2026-19553-wrap-bio
CPython ssl - Python Software Foundation
ssl.SSLContext.wrap_bio() は、check_hostname が設定されている場合に server_hostname が非 None であることを要求していませんでした。SSLObject はホスト名の検証を暗黙的にスキップします。プログラムは check_hostname=True で成功したように見えます。ValueError は発生しません。証明書チェーンは検証されます。ピアの名前は検証されません。wrap_socket() はすでに例外を発生させていました。asyncio の SSLProtocol / start_tls / open_connection は "" を None に変換してから wrap_bio を呼び出します。
別のホスト名に対する CA 有効な証明書を持つ MITM が、server_hostname を渡し忘れた wrap_bio / asyncio クライアントに対してハンドシェイクを完了させます。
| ID | CVE-2026-19553 |
| CWE | CWE-297 |
| CVSS | High: 7.6 CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:P/VC:H/VI:H/VA:N |
| Product | CPython ssl |
| Affected | < 3.12.15、3.13.0–3.13.15、3.14.0–3.14.7、3.15.0a1 (3.15.0 より前) |
| Auth | MITM / 攻撃者の TLS サーバー。被害者はホスト名なしで wrap_bio または asyncio を使用する Python TLS クライアント |
| License | GNU Affero GPL v3.0 |
| Lab | 127.0.0.1 のみ |
経路上に位置する(またはクライアントが到達しようとしていたサーバーになる)ことができます。クライアントが意図した名前とは異なる名前に対して、クライアントが信頼する証明書を提示します。クライアントが wrap_bio(..., server_hostname=None) を使用した場合、または check_hostname=True かつ CERT_REQUIRED の状態で asyncio を server_hostname="" で使用した場合、ハンドシェイクは成功します。ID は一度も検証されませんでした。同じコンテキストでの wrap_socket は、バイトが移動する前に ValueError を発生させます。wrap_bio に空でないホスト名を渡した場合は、依然として不一致を拒否します。
同じ製品、姉妹の残存物: SNI SSLContext UAF。反対側の TLS 側です。これらは組み合わせられません。
PSF が CVE-2026-19553 を投稿しました。Issue python/cpython#156793、PR 158503、コミット 6dc0069a。_check_sslobject_params は wrap_socket に対してすでに実行されていました。SSLObject._create はそれをスキップしていました。NEWS: 証明書チェーンを検証したがピアの ID を検証せずにハンドシェイクを完了し、チェックがスキップされたことを示す表示もない。パッチ適用後、check_hostname=True かつ server_hostname=None/"" での wrap_bio は ValueError("check_hostname requires server_hostname") を発生させます。3.12 バックポートは代わりに DeprecationWarning を発生させます。
python:3.14.7-slim-bookworm を立ち上げました。同じラボ CA、2 つのリーフ: victim.lab と evil.lab。クライアントは check_hostname=True、CERT_REQUIRED。
INJECT: evil.lab に対する wrap_bio(server_hostname=None) が受け入れられました。ピア SAN evil.lab。SNI None。TLS_AES_256_GCM_SHA384。CONTROL A: wrap_socket(server_hostname=None) が ValueError: check_hostname requires server_hostname を発生させました。CONTROL B: evil.lab に対する wrap_bio(server_hostname="victim.lab") が SSLCertVerificationError ホスト名不一致を発生させました。NEGATIVE: victim.lab に対する同じ呼び出しは受け入れられました。追加: 127.0.0.1:18510 での asyncio.open_connection(..., server_hostname="") が受け入れられました("" が None になり、その後 wrap_bio)。
すでに記録された誤った道: 最初の asyncio start_server ドラフトは閉じ括弧を落としました。ホスト側のコンパイルが compose の前にそれを捕捉しました。ホスト 3.14.7(Clang、OpenSSL 3.6.4)は同じ 4 つのオラクルを再現しました。ラボ記録はコンテナのピン(GCC、OpenSSL 3.0.22)です。お芝居: リバースシェル。オラクルは、誤った名前の受け入れと、依然として例外を発生させる wrap_socket です。
cd lab
./run.sh
イメージ python:3.14.7-slim-bookworm。Compose プロジェクト cve-2026-19553。ループバック 127.0.0.1:18510 は asyncio の追加分です。MemoryBIO はポートを必要としません。
INJECT wrap_bio_none vs evil.lab: accept peer=evil.lab
CONTROL_A wrap_socket_none: ValueError:check_hostname requires server_hostname
CONTROL_B wrap_bio_name='victim.lab' vs evil.lab: mismatch-reject
SUCCESS CVE-2026-19553 wrap_bio_none=accept wrap_socket_none=ValueError wrap_bio_name=mismatch-reject CVE-2026-19553-WRAPBIO-HOST-WITNESS
wrap_bio()、asyncio.create_connection()、または loop.start_tls() に空でない server_hostname を渡してください。3.12.15 / 3.13.16 / 3.14.8 / 3.15.0 にアップグレードしてください。パッチは、暗黙のスキップを、wrap_socket がすでに発生させていたのと同じ ValueError に変えるだけです。