Skip to content

fix: count empty continuation frames toward the maxFragments limit#2329

Merged
lpinca merged 4 commits into
websockets:masterfrom
LeSingh1:fix/empty-continuation-frame-bypasses-maxFragments
Jun 22, 2026
Merged

fix: count empty continuation frames toward the maxFragments limit#2329
lpinca merged 4 commits into
websockets:masterfrom
LeSingh1:fix/empty-continuation-frame-bypasses-maxFragments

Conversation

@LeSingh1

Copy link
Copy Markdown
Contributor

Problem

The maxFragments limit in Receiver is intended to cap the number of
message fragments a peer can send before an error is raised with close
code 1008. However, the check was placed inside if (data.length) in
getData(), so zero-byte continuation frames were never counted
against the limit
.

A peer could send one non-empty opening fragment followed by an
unbounded number of empty (payload-length = 0) continuation frames
(opcode 0x00, FIN = 0). Each frame is parsed in getInfo() and
getData() with O(1) work per frame, but the limit never fires.
This allows a remote endpoint to keep the receiver spinning through
startLoop indefinitely, constituting a denial-of-service vector
against any server or client that sets maxFragments.

The same gap existed in the decompress() callback for compressed
fragmented messages.

Fix

Introduce a _numFragments counter (initialized in the constructor and
reset in dataMessage() at the end of every message, alongside
_fragments). The counter is incremented once per data frame in
getData(), before branching on whether the frame is compressed or
whether the payload is non-empty. The two previous `_fragments.length

= _maxFragments` checks are removed; the single early check replaces
both uniformly.

The threshold semantics are preserved: the error fires when the
(_maxFragments + 1)-th frame arrives, matching the previous behaviour
for non-empty frames.

Changes

  • lib/receiver.js — add _numFragments counter; move limit check to
    cover all data frames regardless of payload length or compression.
  • test/receiver.test.js — add a test that sends maxFragments: 2
    with one non-empty opening fragment followed by two empty continuation
    frames and asserts the second empty frame triggers the error.

Test results

436 passing (1s)
receiver.js | 100% Stmts | 100% Branch | 100% Funcs | 100% Lines

Previously the maxFragments check in getData() was guarded by
`if (data.length)`, so zero-byte continuation frames were never
counted against the limit. A malicious peer could send an unlimited
number of empty continuation frames without triggering the
'Too many message fragments' error, bypassing the intended DoS guard.

Fix by introducing a dedicated `_numFragments` counter that is
incremented for every data frame (control frames excluded) before
branching on compression or payload length. The counter replaces the
two separate `_fragments.length >= _maxFragments` checks (one in the
uncompressed path inside getData, one in the decompress callback),
giving consistent enforcement across both paths.

The counter is reset alongside `_fragments` at the end of each
message in dataMessage().

@lpinca lpinca left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Empty fragments are simply discarded so they cause no memory retention problems. It also does not make sense to send them in the first place.

This allows a remote endpoint to keep the receiver spinning through
startLoop indefinitely, constituting a denial-of-service vector
against any server or client that sets maxFragments.

This is not a denial of service. The option was added to keep memory usage under control.

@lpinca

lpinca commented Jun 22, 2026

Copy link
Copy Markdown
Member

Thank you anyway.

@lpinca lpinca closed this Jun 22, 2026
@lpinca lpinca reopened this Jun 22, 2026
@lpinca

lpinca commented Jun 22, 2026

Copy link
Copy Markdown
Member

On second thought this simplifies the code, so I'm good with it.

Comment thread test/receiver.test.js Outdated
Comment thread test/receiver.test.js Outdated
Comment thread test/receiver.test.js Outdated
@lpinca lpinca merged commit 4bc466a into websockets:master Jun 22, 2026
lpinca pushed a commit that referenced this pull request Jun 22, 2026
Ensure that empty fragments are counted against the `maxFragments`
limit.
lpinca pushed a commit that referenced this pull request Jun 22, 2026
Ensure that empty fragments are counted against the `maxFragments`
limit.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants