# Patch Analysis — CVE-2026-54466 (websocket-driver draft-75/76 length header fix)

## Overview

The fix for CVE-2026-54466 was applied in commit `5b197ca` ("Close a draft-75/76 connection if a length header grows to exceed the configured max length"), released as `websocket-driver@0.7.5`. A companion commit `c55679a` added a separate size check for the modern Hybi parser's extension processing path.

## What the Fix Changes

### Files Modified

1. **`lib/websocket/driver/draft75.js`** (commit `5b197ca`, 1 line added)
2. **`lib/websocket/driver/hybi.js`** (commit `c55679a`, 6 lines added — separate fix, not relevant to this variant)

### The Draft-75 Fix (the one relevant to this CVE)

In the `parse()` method's `case 1` (the base-128 length accumulator stage), one line was inserted immediately after accumulation:

```js
case 1:
  this._length = (octet & 0x7F) + 128 * this._length;
  if (this._length > this._maxLength) return this.close();   // <-- ADDED in 0.7.5

  if (this._closing && this._length === 0) {
    return this.close();
  }
  else if ((octet & 0x80) !== 0x80) {
    if (this._length === 0) {
      this._stage = 0;
    }
    else {
      this._skipped = 0;
      this._stage   = 2;
    }
  }
  break;
```

This guard checks after every continuation octet whether the accumulated `_length` exceeds `_maxLength` (`0x3ffffff` = 67,108,863). If it does, the driver is closed immediately via `this.close()`.

## What Assumptions the Fix Makes

The fix assumes that **the only way to trigger message corruption is through an oversized `_length`** — i.e., that the message corruption mechanism requires `_length > _maxLength` to function. Specifically, it assumes:

1. **The length must exceed `_maxLength` for the corruption to occur.** The fix's invariant is: "if `_length <= _maxLength`, the parser state is safe."

2. **The stage-2 skip mode is safe when `_length` is bounded.** The fix assumes that once `_length` is within `_maxLength`, the skip-mode logic in stage 2 correctly consumes exactly `_length` bytes and returns to stage 0 without side effects.

3. **The stale `_buffer` is not a concern.** The fix does not address the fact that `_parseLeadingByte` does NOT reset `this._buffer` when entering binary frame mode (high bit set). The stale buffer from a prior text frame remains referenced.

4. **The `0xFF` check in stage 2 is correct.** The fix does not modify stage 2, where `if (octet === 0xFF)` fires as the very first check — before the skip-mode branch (`if (this._length)`). This means `0xFF` is treated as a frame terminator even in skip mode.

## What Code Paths / Inputs the Fix Does NOT Cover

### Gap 1: `_buffer` not reset in `_parseLeadingByte` (binary frame path)

In `_parseLeadingByte`:
```js
_parseLeadingByte: function(octet) {
  if ((octet & 0x80) === 0x80) {
    this._length = 0;
    this._stage  = 1;
    // NOTE: this._buffer is NOT cleared/reset here
  } else {
    delete this._length;
    delete this._skipped;
    this._buffer = [];
    this._stage  = 2;
  }
}
```

When a binary frame leading byte (high bit set) is received, `_buffer` retains the content from the previous text frame. The fix does not address this.

### Gap 2: `0xFF` treated as frame terminator in skip mode

In stage 2:
```js
case 2:
  if (octet === 0xFF) {          // <-- fires FIRST, before skip/text branching
    this._stage = 0;
    message = Buffer.from(this._buffer).toString('utf8', 0, this._buffer.length);
    this.emit('message', new Base.MessageEvent(message));
  }
  else {
    if (this._length) {          // <-- skip mode (binary frame payload)
      this._skipped += 1;
      if (this._skipped === this._length)
        this._stage = 0;
    } else {                     // <-- text mode (text frame payload)
      this._buffer.push(octet);
      if (this._buffer.length > this._maxLength) return this.close();
    }
  }
  break;
```

The `0xFF` check at the top of `case 2` fires unconditionally — in both skip mode and text mode. In skip mode (binary frame payload consumption), `0xFF` should be treated as a data byte, not a frame terminator. Instead, it emits `Buffer.from(this._buffer)` (the stale buffer from Gap 1) as a message and resets to stage 0.

### Combined Effect (the bypass)

An attacker can:
1. Send a valid text frame → `_buffer` is populated with the message content.
2. Send a binary frame with a **valid** length (`<= _maxLength`) that is `>=` the length of the next frame they want to corrupt → parser enters skip mode with `_buffer` still holding the stale content.
3. Send a valid sentinel text frame → its bytes are consumed as skip data; when its `0xFF` terminator is encountered in skip mode, the stale `_buffer` is emitted as a message, and the sentinel is lost.

This produces the **exact same message corruption** as the original CVE (stale message emission + sentinel loss + connection stays open), but with a `_length` that is well within `_maxLength`, so the fix's guard never fires.

## Whether the Fix Is Complete or Leaves Gaps

**The fix is INCOMPLETE.** It closes the specific vector of oversized length headers but leaves the underlying message-corruption mechanism fully exploitable with valid, bounded length headers. The fix needs to also:

1. **Reset `_buffer` in `_parseLeadingByte`** when entering binary frame mode (or clear it when entering skip mode in `case 1`).
2. **Move the `0xFF` frame-terminator check in stage 2** to only apply in text mode (`_length` is falsy), not in skip mode (`_length` is truthy). Alternatively, in skip mode, `0xFF` should be treated as a data byte.

## Behavior Comparison: Before and After the Fix

| Scenario | 0.7.4 (vulnerable) | 0.7.5 (fixed) |
|---|---|---|
| Oversized length header (`_length > _maxLength`) | Parser enters skip mode with huge `_length`; sentinel consumed/lost; stale message emitted; connection stays open | Parser closes immediately; no corruption |
| Valid length header (`_length <= _maxLength`, `_length >= sentinel_size`) | Parser enters skip mode; sentinel consumed/lost; stale message emitted; connection stays open | **SAME: Parser enters skip mode; sentinel consumed/lost; stale message emitted; connection stays open** |

The fix only changes behavior for the oversized-length case. The valid-length case is identical in both versions — the corruption mechanism works the same way.

## Target Threat Model

No `SECURITY.md` or explicit security policy was found in the repository. The README describes the library as a WebSocket protocol implementation for Node.js. The advisory (GHSA-xv26-6w52-cph6) classifies this as a critical vulnerability. The threat model assumes an attacker who can write raw bytes to an established WebSocket connection using the legacy draft-75/76 protocol. The bypass maintains the same trust boundary (untrusted network peer → server-side parser).
