curl · Report #3971585
33: CONNECT_ONLY raw I/O selects wrong connection after CURLOPT_SHARE detach (incomplete fix for CVE-2020-8231)
How it was found
Summary
After a completed CURLOPT_CONNECT_ONLY transfer, detaching the shared connection pool via curl_easy_setopt(easy, CURLOPT_SHARE, NULL) causes curl_easy_send() / curl_easy_recv() to silently operate on an unrelated connection from a different pool. Data intended for server B is sent to server A, and A's response is accepted as B's.
Tested against commit c2676bf9e639e31e44b12a2cc82cf2046231b056.
Root cause
state.lastconnect_id stores only a bare numeric connection ID (lib/urldata.h:529). Connection IDs are pool-local counters (lib/conncache.c:576), so independent pools both assign ID 0 to their first connection.
Curl_getconnectinfo() resolves lastconnect_id via Curl_cpool_get_conn(), which picks the pool from the easy handle's current share/multi state (lib/conncache.c:256-266), then matches only connection_id (lib/conncache.c:835).
setopt_share() accepts the detach because data->conn is NULL after CONNECT_ONLY (lib/setopt.c:1423-1428), but neither setopt_share() nor Curl_share_easy_unlink() invalidates lastconnect_id. The raw I/O helper easy_connection() only checks that CURLOPT_CONNECT_ONLY is enabled, not that the resolved connection is the one that was created (lib/easy.c:1219-1241).
Result: the retained ID 0 is resolved in the wrong pool, returning an unrelated connection.
Relation to CVE-2020-8231
This is an incomplete-fix variant. CVE-2020-8231's fix replaced a stale connection pointer with a pool-local numeric ID, but the ID is not globally unique. The post-CONNECT_ONLY share-detach transition was not addressed. The underlying pool-local-ID collision was noted in HackerOne #948876 and PR #8524.
Reproduction
Steps
From the curl source root, place the three files above under poc/ and run:
docker build -t curl-33-poc -f poc/Dockerfile .
docker run --rm curl-33-poc
Expected output
SEED_RESULT=0
SHARE_CONNECT_RESULT=0
CONNECT_ONLY_RESULT=0
BEFORE_GETINFO=0
BEFORE_PORT=18081
SHARE_DETACH_RESULT=0
AFTER_GETINFO=0
AFTER_PORT=18080
SEND_RESULT=0
SENT=20
RECV_RESULT=0
RECV=A_REPLY
SERVER_A_MARKER=YES
SERVER_B_MARKER=NO
VERDICT=VULNERABLE
Key observations:
BEFORE_PORT=18081: active socket points to B right afterCONNECT_ONLY.AFTER_PORT=18080: afterCURLOPT_SHARE=NULL, active socket silently switches to A.SERVER_A_MARKER=YES: A receivedSECRET_FOR_SERVER_B.SERVER_B_MARKER=NO: B received nothing.RECV=A_REPLY:curl_easy_recv()returned A's response.
Impact
An attacker controlling server A receives bytes the application intended for server B and can supply bytes accepted as B's response. This is a cross-peer confidentiality and integrity violation.
Practical severity is Low: the attacker cannot force the required API lifecycle. The application must independently (1) keep an attacker-controlled connection alive in the private pool, (2) use a shared connection pool with CONNECT_ONLY, (3) detach the share after the transfer, and (4) then use raw I/O on the handle.