curl · Report #3917775
curl Missing Sec-WebSocket-Accept Verification Enables MITM WebSocket Session Hijacking
How it was found
Summary
curl's Curl_ws_accept() function in lib/ws.c processes the 101 Switching Protocols response to a client WebSocket upgrade request. RFC 6455 Section 4.1 mandates that the client MUST verify the Sec-WebSocket-Accept response header by computing the SHA-1 hash of the concatenation of the Sec-WebSocket-Key value with the magic GUID 258EAFA5-E914-47DA-95CA-C5AB0DC85B11, base64-encoding the result, and comparing it against the server-returned value. The source code contains a comment describing this requirement (line 1418) but implements no actual verification logic — there is no SHA-1 computation, no header comparison, and no rejection path. The function unconditionally calls infof(data, "[WS] Received 101, switch to WebSocket") and switches to WebSocket mode upon receiving any HTTP/1.1 101 Switching Protocols response. A network attacker positioned on the ws:// connection path can return a forged 101 response that entirely omits the Sec-WebSocket-Accept header (or includes an arbitrary incorrect value), and curl will accept it and process attacker-controlled WebSocket frames. This enables full man-in-the-middle hijacking of any ws:// WebSocket transfer.
#!/usr/bin/env python3
"""PoC: send 101 without Sec-WebSocket-Accept — curl accepts it."""
import socket, struct, sys, time
PORT = int(sys.argv[1]) if len(sys.argv) > 1 else 8766
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(("0.0.0.0", PORT)); s.listen(1)
conn, _ = s.accept()
data = b""
while b"\r\n\r\n" not in data:
data += conn.recv(4096)
# Send 101 WITHOUT Sec-WebSocket-Accept header
conn.sendall(b"HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n\r\n")
time.sleep(0.5)
# Send attacker-controlled WebSocket binary frame
frame = bytes([0x82, 25]) + b"ATTACKER_CONTROLLED_DATA"
conn.sendall(frame)
time.sleep(2)
conn.close(); s.close()
Run: python3 poc.py 8766 then in another terminal: curl -v ws://127.0.0.1:8766/
Affected version
curl 8.22.0-DEV (compiled from master, 2026-08-05)
Platform: Ubuntu 24.04 (x86_64), Debug build with ASan
Source location: lib/ws.c, function Curl_ws_accept(), lines 1375–1510
The vulnerability has been present since the initial introduction of WebSocket support. No code changes were observed in the current master branch.
Steps To Reproduce
- Save the PoC script above as
poc.py. - Start the fake WebSocket server:
python3 poc.py 8766 - Run curl:
curl --max-time 5 -v ws://127.0.0.1:8766/ - Observe curl output:
< HTTP/1.1 101 Switching Protocols* Received 101, Switching to WebSocket[WS] Received 101, switch to WebSocket
- curl accepts the 101 response without a
Sec-WebSocket-Acceptheader, switches to WebSocket mode, and processes the attacker-sent frame. No rejection or error occurs.
Expected compliant behavior: curl MUST compute base64(SHA1(Sec-WebSocket-Key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11")) and verify it matches the Sec-WebSocket-Accept value returned by the server. If the header is missing or the value does not match, curl MUST fail the WebSocket connection (RFC 6455 Section 4.1, item 6).
Again: there is no bug-bounty for curl. We do not offer any rewards, only proper credits.
Impact
Summary
An attacker positioned on the network path of a ws:// WebSocket connection can hijack the WebSocket session by returning a forged 101 Switching Protocols response. Because Curl_ws_accept() performs zero verification of the Sec-WebSocket-Accept header, the attacker-controlled 101 response is accepted unconditionally.
Once the forged WebSocket session is established, the attacker gains full control:
- Inject arbitrary WebSocket frames — The attacker can send arbitrary binary or text frames to the curl client, impersonating the legitimate server. Application logic consuming WebSocket data cannot distinguish genuine server data from attacker-injected data.
- Intercept client traffic — Any WebSocket frames the client sends through the hijacked session are visible to the attacker, compromising the confidentiality and integrity of the connection.
- Bypass application-layer authentication — Application-level authentication or message integrity checks built on top of the
ws://WebSocket transport may be undermined, since the attacker controls the entire data channel.
This vulnerability affects all users who use curl's WebSocket support with ws:// URLs. Connections using wss:// (WebSocket over TLS) are not affected, as the TLS layer prevents a man-in-the-middle from tampering with the 101 response.