curl · Report #3973121
11: `CURLOPT_FORBID_REUSE` silently lost on multiplexed HTTP/2 connection when the forbidding transfer finishes first
How it was found
Summary
When HTTP/2 transfers share a multiplexed connection, CURLOPT_FORBID_REUSE set by a transfer that completes before the other active stream(s) is silently discarded. The connection survives into the idle pool and can be reused by a subsequent transfer.
This breaks the mitigation documented in CURLOPT_SSL_CTX_FUNCTION(3): an application that modifies the TLS context via the callback and sets CURLOPT_FORBID_REUSE — exactly as the documentation recommends — will still have that callback-modified connection reused by a later transfer that never ran the callback.
Tested against curl 8.21.0 (tag curl-8_21_0, commit 68720b4837) and 8.22.0-DEV (commit 60870831a825f627545836884466b0179701d23c).
Affected versions:
curl 7.49.0 through 8.21.0, plus tested 8.22.0 prerelease/development versions. Dynamically confirmed on 8.21.0 and 8.22.0-DEV.
Root cause
In lib/multi.c, multi_done_locked() detaches the completing transfer and returns immediately when another stream remains attached:
Curl_detach_connection(data);
if(CONN_INUSE(conn)) {
/* Stop if still used. */
return;
}
The return happens before multi_conn_should_close() evaluates data->set.reuse_forbid. No close-on-idle state (conn->bits.close, conn->bits.no_reuse) is persisted on the shared connection. When the final, non-forbidding transfer later completes, its settings control the close decision and the connection enters Curl_cpool_conn_now_idle().
Impact
An CURLOPT_SSL_CTX_FUNCTION callback may alter trust anchors, client identity, or TLS protocol version — changes curl cannot represent in its connection-matching state. The documentation explicitly recommends setting CURLOPT_FORBID_REUSE to prevent the modified connection from being reused. In the multiplexing scenario, this documented mitigation fails silently: a later transfer reuses the callback-modified TLS connection without the callback ever running again.
Severity is Low: exploitation requires the multi interface, HTTP/2 multiplexing, CURLOPT_SSL_CTX_FUNCTION, and attacker-influenceable completion ordering (typically a malicious server controlling stream timing).
Reproduction
Reproduction steps
- Build an affected curl release, starting with 7.49.0, with HTTP/2 multiplexing support and a TLS backend that supports
CURLOPT_SSL_CTX_FUNCTION. - Start a TLS-enabled HTTP/2 server that can serve multiple streams on one connection, delay individual responses, and report a unique identifier for each underlying TCP connection.
- Create one libcurl multi handle, enable HTTP/2 multiplexing, and restrict the connection pool to one connection for the test origin.
- Create two compatible easy handles targeting different paths on the same HTTPS origin so that curl multiplexes both transfers over one HTTP/2 connection.
- Configure the first transfer with
CURLOPT_SSL_CTX_FUNCTION. In the callback, make a measurable TLS-context change and setCURLOPT_FORBID_REUSEon that easy handle, following the option's documented guidance. - Leave the second transfer without the callback and without
CURLOPT_FORBID_REUSE. - Have the server complete the callback-bearing transfer first while keeping the second HTTP/2 stream active.
- Confirm that both transfers use the same underlying connection and that the forbidding transfer reaches completion before the non-forbidding transfer.
- Allow the second transfer to complete, then remove and clean up both completed easy handles while retaining the multi handle and its connection pool.
- Add a third easy handle for the same origin without the TLS callback and perform another transfer.
- Inspect the server's connection identifier and
CURLINFO_NUM_CONNECTSfor the third transfer. - Observe the vulnerable result: the third transfer uses the original callback-modified connection and reports that it opened no new connection, despite the first transfer having requested
CURLOPT_FORBID_REUSE. - Repeat the scenario with
CURLOPT_FORBID_REUSEplaced on the last-finishing stream instead. - Observe the control result: after the final stream completes, curl closes the original connection and the third transfer opens a new one.
- Confirm a fix by persisting the first completing transfer's no-reuse decision on the shared connection before detaching it. In the original ordering, the connection should close as soon as its remaining streams finish, and the third transfer must create a new connection.
Expected result
If any transfer using a multiplexed connection requests CURLOPT_FORBID_REUSE, that connection must be marked for closure and must not return to the idle pool after its remaining streams complete.
Vulnerable result
When the forbidding transfer finishes before another stream, curl discards its no-reuse decision. The last non-forbidding stream determines the connection's fate, allowing a later transfer to reuse the callback-modified TLS connection.
Suggested fix
In multi_done_locked(), before the CONN_INUSE early return, evaluate the completing transfer's reuse_forbid and persist it on the connection (e.g. set conn->bits.close or conn->bits.no_reuse), retaining the existing NTLM/Negotiate handshake exceptions. Add a regression test with two multiplexed HTTP/2 streams where the forbidding stream finishes first, then assert that a third transfer creates a new connection.
Impact
Summary:
A server that controls HTTP/2 stream completion timing can cause a callback-modified TLS connection to survive despite CURLOPT_FORBID_REUSE and be reused by later transfers. Depending on the callback’s changes, subsequent requests may use an unintended client identity, trust policy, or TLS configuration, potentially causing authentication-context confusion or exposing protected traffic to an endpoint the application intended to distrust.