# REPRO-2026-00292: websocket-driver: message corruption via abuse of draft-WebSocket length headers ## Summary Status: published Severity: critical Type: security Confidence: high ## Identifiers REPRO ID: REPRO-2026-00292 CVE: CVE-2026-54466 ## Package Name: faye/websocket-driver-node Ecosystem: npm Affected: < 0.7.5 Fixed: 0.7.5 ## Root Cause # RCA Report — CVE-2026-54466 ## Summary `websocket-driver` (npm package `websocket-driver`, repo `faye/websocket-driver-node`) versions before 0.7.5 mishandle an overlong variable-width length header in the legacy draft-75 / draft-76 (hixie-75 / hixie-76) WebSocket parser. The stage-1 base-128 length accumulator in `lib/websocket/driver/draft75.js` (`this._length = (octet & 0x7F) + 128 * this._length`) has no upper bound, so a small, attacker-controlled wire sequence makes the declared `_length` exceed `_maxLength` (`0x3ffffff` = 67108863). The driver stays open and enters stage-2 "skip" mode with a gigantic `_length`, which then consumes, loses, or misframes a subsequent valid text frame instead of delivering its intended message boundary. The 0.7.5 fix adds a per-octet guard `if (this._length > this._maxLength) return this.close();` immediately after accumulation. ## Impact - **Package/component affected:** `websocket-driver`, the `Draft75`/`Draft76` parser path (`lib/websocket/driver/draft75.js`), reached via `Driver.server(...)` / `Server.parse(chunk)`. - **Affected versions:** `websocket-driver < 0.7.5` (representative vulnerable build `0.7.4`; fixed control `0.7.5`). - **Risk level and consequences:** Critical protocol-level message corruption / message loss on any server that accepts legacy hixie-75/76 framing. An attacker who can write raw bytes to an established connection can cause the parser to drop the framing boundary of the following legitimate frame and emit a stale/misframed application message, defeating message integrity. This is a denial-of-integrity / message-corruption issue, not RCE and not memory exhaustion; the wire allocation stays tiny. ## Impact Parity - **Disclosed/claimed maximum impact:** Protocol message corruption / message loss (impact class `other`), via abuse of the draft-75/76 length header on a negotiated hixie-75/76 connection. - **Reproduced impact from this run:** On `0.7.4`, the driver remained open (`readyState=1`) after the oversized length header, did **not** deliver the uniquely-marked sentinel text frame, and instead re-emitted a **stale** copy of the previous (positive-control) message — i.e. the sentinel was consumed/misframed. On `0.7.5`, the identical stream closed (`readyState=3`, `close` event) while accumulating the over-limit length header and emitted no corrupted application message. Positive control delivered in both builds, proving the parser/listener path was operational. - **Parity:** `full` — the reproduced behavior matches the disclosed message-corruption/message-loss impact and the fixed-build negative control exactly. - **Not demonstrated:** No code execution, no memory exhaustion, no native crash — none claimed. ## Root Cause In `Draft75.prototype.parse` the per-octet state machine has three relevant stages: - **stage 0** (`_parseLeadingByte`): a leading byte with the high bit set (`(octet & 0x80) === 0x80`) initializes `_length = 0` and moves to **stage 1** (binary/control frame with a variable-width length header). - **stage 1** (length accumulator): for every octet, `_length = (octet & 0x7F) + 128 * this._length`. An octet whose high bit is clear is the terminator; if `_length !== 0` the parser transitions to **stage 2** with `_skipped = 0`. - **stage 2** (skip mode): while `_length` is truthy, each incoming octet increments `_skipped` until `_skipped === _length`; the only early exit is `octet === 0xFF`, which emits `Buffer.from(this._buffer)` as a message and resets to stage 0. Crucially, in the high-bit-set leading-byte branch `_parseLeadingByte` does **not** reset `this._buffer`, so the stale buffer from a prior text frame is still referenced. In versions `< 0.7.5` there is **no bound check** on `_length` during stage 1. The minimal deterministic sequence ``` 0x80 0xFF 0xFF 0xFF 0x7F ``` drives the accumulator: `0 → 127 → 16383 → 2097151 → 268435455`. The terminating `0x7F` (high bit clear) produces `_length = 268435455`, which exceeds `_maxLength` (`0x3ffffff` = `67108863`), and the parser falls through to stage-2 skip mode. A following valid sentinel text frame (`0x00 0xFF`) is then consumed byte-by-byte as "skipped" payload; when the sentinel's `0xFF` terminator is reached, stage 2 emits `Buffer.from(this._buffer)` — the **stale** previous message (`POSITIVE-CTRL-OK`) — and resets to stage 0. The sentinel text is never delivered; a corrupted/stale message is delivered in its place. The connection stays open, so the corruption is silent and ongoing. The 0.7.5 patch (`lib/websocket/driver/draft75.js`, one added line in `case 1`) inserts the guard immediately after accumulation: ```js this._length = (octet & 0x7F) + 128 * this._length; if (this._length > this._maxLength) return this.close(); // <-- added in 0.7.5 ``` so the over-limit header closes the driver (`readyState=3`, `close` event) before any sentinel is accepted and before any corrupted message can be emitted. Fix reference: `websocket-driver@0.7.5` release — https://github.com/faye/websocket-driver-node/releases/tag/0.7.5 ; advisory GHSA-xv26-6w52-cph6. ## Reproduction Steps 1. Reference: `bundle/repro/reproduction_steps.sh` (driver script) and `bundle/repro/harness.js` (Node TCP peer harness). Both are self-contained; the script reuses the prepared project cache (`bundle/project_cache_context.json`, `prepared=true`) when available and otherwise falls back to `npm install websocket-driver@0.7.4` / `@0.7.5`. 2. What the script does: - Resolves the vulnerable (`0.7.4`) and fixed (`0.7.5`) published `websocket-driver` packages from the durable project cache (or installs them). - Verifies the versions and the presence/absence of the `_maxLength` guard in `draft75.js` for each build. - For each build, runs `harness.js`, which starts a real loopback TCP server using `net.createServer` + `Driver.server({requireMasking:false})`, pipes `connection ↔ driver.io`, and a raw TCP client performs a draft-75 handshake (no `sec-websocket-*` headers, so `Server.http` selects `Draft75`), waits for the `101` response, then sends (a) a positive-control text frame `0x00 "POSITIVE-CTRL-OK" 0xFF`, then (b) the malicious overlong length header `0x80 0xFF 0xFF 0xFF 0x7F` immediately followed by (c) a uniquely-marked sentinel text frame `0x00 "SENTINEL-VULN-XYZ-12345" 0xFF`. All emitted `open`/`message`/`close`/`error` events, the `readyState`, and the exact wire bytes are recorded to JSON. - Evaluates the oracle: vulnerable build must deliver the positive control, stay open (no `close`), **not** deliver the sentinel, and emit a corrupted/stale message; fixed build must deliver the positive control, emit `close` on the malicious header, and emit no corrupted message. - Writes `bundle/repro/runtime_manifest.json`, `bundle/repro/validation_verdict.json`, and best-effort copies proof artifacts into the project-cache proof-carry directory. 3. Expected evidence of reproduction (see `bundle/logs/oracle_eval.txt`, `bundle/logs/vuln_result.json`, `bundle/logs/fixed_result.json`): - **Vulnerable 0.7.4:** `positive_delivered=true`, `sentinel_delivered=false`, `close_after_malicious=false`, `corrupted_message_after_malicious=true`, `positive_count=2` (stale re-emit), `readyState_final=1`. - **Fixed 0.7.5:** `positive_delivered=true`, `sentinel_delivered=false`, `close_after_malicious=true`, `corrupted_message_after_malicious=false`, `positive_count=1`, `readyState_final=3`. - Oracle `CONFIRMED=true` only when both paths match. ## Evidence - `bundle/logs/reproduction_steps.log` — full driver script transcript. - `bundle/logs/vuln_result.json` — events + wire bytes for `0.7.4`. - `bundle/logs/fixed_result.json` — events + wire bytes for `0.7.5`. - `bundle/logs/vuln_run.log`, `bundle/logs/fixed_run.log` — raw harness stdout per build. - `bundle/logs/oracle_eval.txt` — oracle evaluation summary. - `bundle/repro/runtime_manifest.json` — runtime manifest (`entrypoint_kind=tcp_peer`, `service_started=true`, `healthcheck_passed=true`, `target_path_reached=true`). - `bundle/repro/validation_verdict.json` — structured verdict (`claim_outcome=confirmed`). Key excerpt (vulnerable 0.7.4 events): ```json [ {"type":"open"}, {"type":"message","data":"POSITIVE-CTRL-OK"}, {"type":"message","data":"POSITIVE-CTRL-OK"} // stale re-emit; sentinel "SENTINEL-VULN-XYZ-12345" never delivered ] ``` Key excerpt (fixed 0.7.5 events): ```json [ {"type":"open"}, {"type":"message","data":"POSITIVE-CTRL-OK"}, {"type":"close"} // guard closes on over-limit length header; no corrupted message ] ``` Wire bytes (hex): malicious header `80ffffff7f` → computed `_length = 268435455 > maxLength 67108863`; sentinel `0053454e54494e454c2d56554c4e2d58595a2d3132333435ff`. Environment: Node.js v24.x on Linux; `net` loopback TCP; `websocket-driver` 0.7.4 (vulnerable) and 0.7.5 (fixed) from the published npm package; no sanitizers (production path, `sanitizer_used=false`). ## Recommendations / Next Steps - **Upgrade:** Pin `websocket-driver` to `>= 0.7.5` (the guard `if (this._length > this._maxLength) return this.close();` is the canonical fix). - **Defense in depth:** Where legacy hixie-75/76 framing is not required, disable draft-75/76 negotiation at the HTTP upgrade layer so `Server.http` never selects `Draft75`/`Draft76`. - **Testing:** Add a regression test that feeds `0x80 0xFF 0xFF 0xFF 0x7F` + a sentinel text frame through `Driver.server()` over a real socket and asserts (1) a `close` event fires and (2) no `message` event carries stale/sentinel content. Also assert the positive-control text frame still delivers. - **Variant exploration (next stage):** Confirm the same accumulator path is reachable through `Draft76` (hixie-76) once the 8-byte challenge body is satisfied, and check whether intermediate `_length` values just under `_maxLength` can still cause partial misframing without tripping the guard. ## Additional Notes - **Idempotency:** The script was run twice consecutively with identical results (oracle `CONFIRMED=true` both times, identical event shapes and wire bytes); the only nondeterministic field is the event timestamp `t`. - **Minimal deterministic octet sequence derived from shipped source:** `0x80 0xFF 0xFF 0xFF 0x7F` (5 bytes) drives `_length` to `268435455`, the smallest terminator-using sequence that exceeds `_maxLength` (`67108863`) using all-high-bit continuation octets. The physical wire input (header + sentinel) is under 40 bytes — no resource exhaustion. - **Real package, real socket:** The proof exercises the **published** `websocket-driver` package through `Driver.server(...).parse(chunk)` over a real loopback TCP socket (not a copied parser or reimplementation), satisfying the `network_protocol` / `tcp_peer` entrypoint requirement. - **Limitations:** The oracle uses an in-process `net` loopback peer; no external network is involved. The driver's `close()` in the fixed build ends the connection before the sentinel, so `sentinel_delivered=false` in both builds — the discriminator is the `close` event plus the absence of a corrupted/stale message in the fixed build versus the stale re-emit + open state in the vulnerable build. ## Reproduction Details Reproduced: 2026-07-16T11:51:35.032Z Duration: 448 seconds Tool calls: 127 Turns: Unknown Handoffs: 2 ## Quick Verification Run one of these commands to verify locally: pruva-verify REPRO-2026-00292 pruva-verify CVE-2026-54466 Or open in GitHub Codespaces (zero-friction, auto-runs): https://github.com/codespaces/new?ref=repro/REPRO-2026-00292&repo=N3mes1s/pruva-sandbox Or download and run the script manually: curl -O https://api.pruva.dev/v1/reproductions/REPRO-2026-00292/artifacts/bundle/repro/reproduction_steps.sh chmod +x reproduction_steps.sh ./reproduction_steps.sh WARNING: Run in a sandboxed environment. This exploits a real vulnerability. ## References - NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-54466 - Source: https://github.com/faye/websocket-driver-node ## Artifacts - bundle/repro/reproduction_steps.sh (reproduction_script, 11813 bytes) - bundle/repro/rca_report.md (analysis, 11301 bytes) - bundle/vuln_variant/reproduction_steps.sh (reproduction_script, 7924 bytes) - bundle/vuln_variant/rca_report.md (analysis, 11911 bytes) - bundle/repro/harness.js (other, 7021 bytes) - bundle/repro/validation_verdict.json (other, 926 bytes) - bundle/repro/runtime_manifest.json (other, 1578 bytes) - bundle/logs/reproduction_steps.log (log, 1898 bytes) - bundle/logs/vuln_result.json (other, 765 bytes) - bundle/logs/fixed_result.json (other, 730 bytes) - bundle/logs/vuln_run.log (log, 766 bytes) - bundle/logs/fixed_run.log (log, 731 bytes) - bundle/logs/oracle_eval.txt (other, 1165 bytes) - bundle/vuln_variant/variant_harness.js (other, 7866 bytes) - bundle/vuln_variant/patch_analysis.md (documentation, 6864 bytes) - bundle/vuln_variant/variant_manifest.json (other, 3236 bytes) - bundle/vuln_variant/validation_verdict.json (other, 1968 bytes) - bundle/vuln_variant/source_identity.json (other, 970 bytes) - bundle/vuln_variant/runtime_manifest.json (other, 1828 bytes) - bundle/vuln_variant/root_cause_equivalence.json (other, 1975 bytes) ## API Access - JSON: https://api.pruva.dev/v1/reproductions/REPRO-2026-00292 - Script: https://api.pruva.dev/v1/reproductions/REPRO-2026-00292/artifacts/bundle/repro/reproduction_steps.sh - Web: https://pruva.dev/r/REPRO-2026-00292 ## For AI Assistants To help users with this reproduction: 1. The `pruva-verify` command is the simplest way to run the reproduction 2. The script downloads and runs automatically in an isolated temp directory 3. Always recommend running in a VM, container, or disposable environment 4. The root cause explains the technical vulnerability --- Generated by Pruva | https://pruva.dev