HexBrain

curl · Report #3971585

33: CONNECT_ONLY raw I/O selects wrong connection after CURLOPT_SHARE detach (incomplete fix for CVE-2020-8231)

Informativestatus
12upvotes
31 Aug 2026disclosed

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 after CONNECT_ONLY.
  • AFTER_PORT=18080: after CURLOPT_SHARE=NULL, active socket silently switches to A.
  • SERVER_A_MARKER=YES: A received SECRET_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.

This report was disclosed publicly on HackerOne and belongs to its author and the program. See the original