curl · Report #3971496
28: HTTP/3 UDP path ignores CURL_SOCKOPT_ALREADY_CONNECTED, reconnects callback-provided socket
How it was found
Summary
When CURLOPT_SOCKOPTFUNCTION returns CURL_SOCKOPT_ALREADY_CONNECTED, the HTTP/3 (QUIC) UDP code path ignores it and calls connect() anyway, silently reassociating the application-provided socket to a different peer. The TCP path has the correct check. This violates the documented callback contract and can bypass an application-controlled relay or egress route.
The same class of bug was previously reported for TCP in #10626 and fixed by #10648 (commit 233810bb), but the equivalent UDP path was missed.
Root cause
In lib/cf-socket.c, cf_socket_open() records CURL_SOCKOPT_ALREADY_CONNECTED by setting cf->connected = TRUE.
cf_tcp_connect() checks this flag immediately after cf_socket_open() returns and short-circuits:
// lib/cf-socket.c, inside cf_tcp_connect(), after cf_socket_open():
if(cf->connected) {
*done = TRUE;
return CURLE_OK;
}
cf_udp_connect() has no such check. After cf_socket_open() sets cf->connected, it proceeds unconditionally into cf_udp_setup_quic(), which calls connect() with the URL's destination address, overwriting the application's chosen peer on the same file descriptor.
Reproduction
Expected output (port numbers will vary):
Vulnerable build:
VERDICT=VULNERABLE
The socket's getpeername() reports endpoint B (the URL target), B received 2 QUIC Initial datagrams (2400 bytes), A received nothing. The SO_BROADCAST marker set in the sockopt callback persists, confirming this is the same file descriptor, not a replacement socket.
Patched control:
VERDICT=FIXED
The socket's getpeername() still reports endpoint A (the callback-chosen peer), A received the 2 QUIC Initial datagrams, B received nothing.
The final line VERDICT_VALIDATED=YES confirms both checks passed.
No HTTP/3 server is required; the PoC uses two bare UDP listeners. The timeout (CURLE_OPERATION_TIMEDOUT) is expected because neither listener answers the QUIC handshake.
Impact
An application can use CURLOPT_OPENSOCKETFUNCTION + CURLOPT_SOCKOPTFUNCTION to supply a UDP socket already connected through a relay, policy boundary, or selected egress endpoint. With HTTP/3, curl silently reassociates that socket with its own resolved destination and sends pre-authentication QUIC Initial datagrams there instead.
Preconditions for exposure:
- HTTP/3 is used
- The application supplies an already-connected UDP descriptor via
CURLOPT_OPENSOCKETFUNCTION CURLOPT_SOCKOPTFUNCTIONreturnsCURL_SOCKOPT_ALREADY_CONNECTED
This is a specialized callback arrangement. I have not identified a production application relying on it, so I am reporting the confirmed contract and routing violation for the security team to assess rather than asserting a particular severity.