HexBrain

curl · Report #3971496

28: HTTP/3 UDP path ignores CURL_SOCKOPT_ALREADY_CONNECTED, reconnects callback-provided socket

Informativestatus
lowseverity
3upvotes
28 Aug 2026disclosed

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:

  1. HTTP/3 is used
  2. The application supplies an already-connected UDP descriptor via CURLOPT_OPENSOCKETFUNCTION
  3. CURLOPT_SOCKOPTFUNCTION returns CURL_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.

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