HexBrain

curl · Report #3917775

curl Missing Sec-WebSocket-Accept Verification Enables MITM WebSocket Session Hijacking

N/Astatus
37upvotes
5 Aug 2026disclosed

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

  1. Save the PoC script above as poc.py.
  2. Start the fake WebSocket server: python3 poc.py 8766
  3. Run curl: curl --max-time 5 -v ws://127.0.0.1:8766/
  4. Observe curl output:
    • < HTTP/1.1 101 Switching Protocols
    • * Received 101, Switching to WebSocket
    • [WS] Received 101, switch to WebSocket
  5. curl accepts the 101 response without a Sec-WebSocket-Accept header, 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.

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