CVE-2026-91992: Tornado · CurlAsyncHTTPClient leaks per-request credentials on handle reuse
Affects sickchillSource GCVE Database
Not known to be exploited · CVSS 8.2 High : upgrade to 2024.3.1_6 soon.
Data as of 30 Oct 2026, 07:30 UTC
- Patch
- Available – fixed in 2024.3.1_6
CVSS:4.0/AV:N/AC:H/AT:P/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:NExploitation evidence
Disclosed: evidence found.
CVE published 15 Sep 2026
Detectable: no evidence on record.
No public nuclei template indexed
Weaponized: no evidence on record.
No public exploit code indexed in Metasploit or Exploit-DB
Exploited in the wildExploited: no evidence on record.
Not in CISA KEV and no source reports exploitation
Observed by OffSeq MirageSeen by Mirage: no evidence on record.
No attack traffic recorded by OffSeq Mirage sensors
A missing step is not evidence of safety. Sources: cve.org (opens in a new tab), the Metasploit, Exploit-DB and nuclei-templates index as of 29 Oct 2026.
What to do
Upgrade sickchill to 2024.3.1_6.
Affected software
| Product | Affected | Fixed in |
|---|---|---|
Homebrewsickchillpkg:brew/sickchillOther threats | Affected
| Fixed in
|
Check your own hosts for these packages with threat-finder (opens in a new tab), our free open-source scanner.
Description
# CurlAsyncHTTPClient leaks per-request credentials on handle reuse ## Summary `CurlAsyncHTTPClient` pools and reuses `pycurl` handles across requests but does not reset them between requests, and several per-request options are applied with no clearing branch. As a result, sensitive state set by one request persists onto a later request on the same client that does not set it. Two credential vectors are demonstrated below — a client TLS certificate (`SSLCERT`/`SSLKEY`) and proxy basic-auth credentials (`PROXYUSERPWD`) — both leaking to a different, unintended host. This affects all released versions through 6.5.6. ## Details In `tornado/curl_httpclient.py`, handles are created once and returned to a free list for reuse (`_process_queue` pops the handle at line 200, `_finish` re-appends it at line 245), and `_curl_setup_request` is never preceded by `curl.reset()`. The function clears *some* carried-over state on the reused handle — `unsetopt(PROXYUSERPWD)` in the no-proxy branch (line 394), `unsetopt(USERPWD)` when no auth is set (line 495), and the HTTP-method flag reset (lines 428-432) — but other options have no equivalent clearing path and persist until a later request sets them again. **Vector A — client TLS certificate (`SSLCERT`/`SSLKEY`).** Set-only, no clearing branch: ```python # tornado/curl_httpclient.py (v6.5.6), lines 498-502 if request.client_cert is not None: curl.setopt(pycurl.SSLCERT, request.client_cert) if request.client_key is not None: curl.setopt(pycurl.SSLKEY, request.client_key) ``` A request that sets `client_cert` leaves the certificate on the handle; a later request without `client_cert` presents it during its TLS handshake. **Vector B — proxy credentials (`PROXYUSERPWD`).** `PROXYUSERPWD` is set only inside the credentials branch and unset only in the no-proxy `else` branch: ```python # tornado/curl_httpclient.py (v6.5.6), lines 371-394 if request.proxy_host and request.proxy_port: curl.setopt(pycurl.PROXY, request.proxy_host) curl.setopt(pycurl.PROXYPORT, request.proxy_port) if request.proxy_username: # only place PROXYUSERPWD is set ... curl.setopt(pycurl.PROXYUSERPWD, credentials) ... else: try: curl.unsetopt(pycurl.PROXY) except TypeError: curl.setopt(pycurl.PROXY, "") curl.unsetopt(pycurl.PROXYUSERPWD) # only place it is unset ``` A request that sets a *new* `proxy_host` without `proxy_username` updates `PROXY`/`PROXYPORT` but never reaches the `else`, so the previous request's credentials persist and are sent to the new proxy. The same class also affects `INTERFACE` (lines 365-366: set only when `request.network_interface` is truthy, with no clearing branch), which is a lower-severity instance — a later request can be bound to a network interface it did not request. A single fix addresses all three (see Mitigation). ## PoC Both reproduce against the pinned release using public API only (`CurlAsyncHTTPClient`, `HTTPRequest`, and the documented per-request arguments). ### Vector A — client TLS certificate The two servers listen on different ports, so request B opens a fresh TCP+TLS connection; the certificate can only reach server 2 via the persisted handle option, not connection or session reuse. ``` python3 -m venv venv ./venv/bin/pip install "tornado==6.5.6" pycurl cryptography ./venv/bin/python poc_client_cert.py ``` ```python import asyncio import datetime import ipaddress import os import socket import ssl import sys import tempfile import threading from cryptography import x509 from cryptography.x509.oid import NameOID, ExtendedKeyUsageOID from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import rsa from tornado.httpclient import HTTPRequest from tornado.curl_httpclient import CurlAsyncHTTPClient def _key(): return rsa.generate_private_key(public_exponent=65537, key_size=2048) def _ca(): key = _key() name = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, "PoC-CA")]) now = datetime.datetime.now(datetime.timezone.utc) cert = ( x509.CertificateBuilder() .subject_name(name).issuer_name(name) .public_key(key.public_key()) .serial_number(x509.random_serial_number()) .not_valid_before(now - datetime.timedelta(minutes=1)) .not_valid_after(now + datetime.timedelta(days=1)) .add_extension(x509.BasicConstraints(ca=True, path_length=None), critical=True) .sign(key, hashes.SHA256()) ) return cert, key def _leaf(cn, ca_cert, ca_key, ips=None, client=False): key = _key() name = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, cn)]) now = datetime.datetime.now(datetime.timezone.utc) b = ( x509.CertificateBuilder() .subject_name(name).issuer_name(ca_cert.subject) .public_key(key.public_key()) .serial_number(x509.random_serial_number()) .not_valid_before(now