GHSA-pw6j-qg29-8w7f: Tornado: CurlAsyncHTTPClient leaks per-request credentials on handle reuse
CurlAsyncHTTPClient leaks per-request credentials on handle reuseSummary`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.DetailsIn `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: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: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).PoCBoth reproduce against the pinned release using public API only
(`CurlAsyncHTTPClient`, `HTTPRequest`, and the documented per-request arguments).Vector A — client TLS certificateThe 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.Output (`pip show tornado` → 6.5.6, installed in the venv):Vector B — proxy credentialsEach proxy is a separate listener capturing the raw request bytes.Output (`YWxpY2U6c2VjcmV0QQ==` decodes to `alice:secretA`):Impact**Type:** Exposure of credentials to an unintended party (CWE-200), via reuse
of a resource whose sensitive state was not cleared (CWE-672).**Actors:** An application that issues requests with differing per-request
options on a shared `CurlAsyncHTTPClient` — for Vector A, mixing per-request
`client_cert` requests with non-certificate requests; for Vector B,
multiplexing requests across more than one proxy with per-proxy credentials.**Effect:** For Vector A, the client completes the TLS client-authentication
handshake — proving possession of the private key and disclosing the
certificate subject and chain — to a host that was never meant to receive it.
For Vector B, proxy basic-auth credentials are transmitted (base64) to a
different proxy. If the unintended host/proxy is attacker-controlled or
attacker-influenced (a user-supplied URL, webhook target, SSRF-reachable
endpoint, or a proxy chosen from user-controlled configuration), the credential
is disclosed to the attacker.**Scope:** Only applications using the optional `CurlAsyncHTTPClient` backend
with the patterns above are affected. The default `SimpleAsyncHTTPClient` is not
affected (and does not support proxies).Proposed CWE: CWE-200 / CWE-672. Proposed CVSS 3.1:
`CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N` (5.9, medium); attack complexity is
High because exploitation depends on the application using differing per-request
options on a shared client and on handle scheduling.MitigationA single fix closes all instances of this class: call `curl.reset()` at the start
of `_curl_setup_request` and then re-apply the per-request options, so no state
from a prior request can persist on the reused handle. (Note `curl.reset()` also
clears `CAINFO`, which the current code intentionally leaves untouched — see the
comment at lines 401-409 — so that default would need to be re-established after
the reset.)Alternatively, add explicit clearing branches mirroring the existing
`PROXYUSERPWD`/`USERPWD` handling:Until a fix is available, use a separate `CurlAsyncHTTPClient` instance per
distinct credential set (per client certificate / per proxy credential), or use
`SimpleAsyncHTTPClient` where applicable.
CVSS v3.1: 5.9