WebSocket: cap the 101 handshake head on the completed parse path
Closed
RO
2mo ago+62+62+01234567890123456789-0-0-0123456789
Problem
new WebSocket()accepts a 101 handshake head larger than the cap (--max-http-header-size, default 16 KB) whenever norecv()boundary lands inside the incomplete head. A 60 KB head split so the first read stays under the cap opens the connection.- The cap is checked only in the
ShortReadarm ofbuffer_and_parse_head(src/http_jsc/websocket_client/WebSocketUpgradeClient.rs:717). TheOk(done)arm, which runs when the head parse completes, never checks the size. So the bound depends on read segmentation, not on the head size.
Fix
- Check the cap in the
Ok(done)arm too. Comparehead_len(the header size alone), so pipelined WebSocket frames after the head are not counted. - Correct because the cap now applies to the header bytes on every path, not only when a read boundary splits an incomplete head. This matches the existing
ShortReadcheck and Node and undici, which enforce a fixed 16 KB regardless of segmentation. - Verified:
test/js/web/websocket/websocket-client-short-read.test.ts(new case, stock bun opens the connection and fails). Also ran the other 4 cases in that file, including the pipelined large-frame case that must still open.
Background
- A WebSocket client first sends an HTTP/1.1 upgrade request, then reads the 101 response head.
buffer_and_parse_headaccumulates bytes and runspicohttp::Response::parse. parsereturnsShortReadwhile no\r\n\r\nis found, andDoneonce the head is complete.head_len(response.bytes_read) is the head size. Bytes after it are pipelined WebSocket frames.max_http_header_size(default 16 KB) bounds the head so a peer cannot force unbounded buffering.
[human-review] gate passed · iteration 1 · 2 files touched
fails on main (without fix)
ASAN without fix: 1 FAILED
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/pr_gate.xml" test/js/web/websocket/websocket-client-short-read.test.ts
bun test v1.4.3 (f42e98025)
test/js/web/websocket/websocket-client-short-read.test.ts:
(pass) WebSocket > short read on payload length [99.64ms]
(pass) WebSocket upgrade split across reads > large frame pipelined after split 101 header is not counted against the header-size cap [108.04ms]
239 | },
240 | });
241 |
242 | const { promise, resolve, reject } = Promise.withResolvers<string>();
243 | const ws = new globalThis.WebSocket(`ws://127.0.0.1:${server.port}`);
244 | ws.onopen = () => reject(new Error("unexpected open"));
^
error: unexpected open
at <anonymous> (/workspace/bun/test/js/web/websocket/websocket-client-short-read.test.ts:244:34)
(fail) WebSocket upgrade split across reads > completed header larger than the cap is rejected even when split under the cap [102.86ms]
(pass) WebSocket upgrade split across reads > incomplete header larger than the cap is still rejected [84.91ms]
(pass) WebSocket buffered
... (truncated)
release without fix: 1 FAILED
bun test v1.4.3-canary.1 (f42e98025)
test/js/web/websocket/websocket-client-short-read.test.ts:
(pass) WebSocket > short read on payload length [2.92ms]
(pass) WebSocket upgrade split across reads > large frame pipelined after split 101 header is not counted against the header-size cap [51.35ms]
239 | },
240 | });
241 |
242 | const { promise, resolve, reject } = Promise.withResolvers<string>();
243 | const ws = new globalThis.WebSocket(`ws://127.0.0.1:${server.port}`);
244 | ws.onopen = () => reject(new Error("unexpected open"));
^
error: unexpected open
at <anonymous> (/workspace/bun/test/js/web/websocket/websocket-client-short-read.test.ts:244:34)
(fail) WebSocket upgrade split across reads > completed header larger than the cap is rejected even when split under the cap [53.38ms]
(pass) WebSocket upgrade split across reads > incomplete header larger than the cap is still rejected [51.21ms]
(pass) WebSocket buffered handshake data > terminating the client from its open handler while handshake bytes are buffered shuts down cleanly [13.24ms]
4 pass
1 fail
5 expect() calls
Ran 5 tests across 1 fi
... (truncated)
passes on PR (with fix)
ASAN with fix: all passed
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/pr_gate.xml" test/js/web/websocket/websocket-client-short-read.test.ts
bun test v1.4.3 (f42e98025)
test/js/web/websocket/websocket-client-short-read.test.ts:
(pass) WebSocket > short read on payload length [81.49ms]
(pass) WebSocket upgrade split across reads > large frame pipelined after split 101 header is not counted against the header-size cap [1390.69ms]
(pass) WebSocket upgrade split across reads > completed header larger than the cap is rejected even when split under the cap [80.45ms]
(pass) WebSocket upgrade split across reads > incomplete header larger than the cap is still rejected [83.33ms]
(pass) WebSocket buffered handshake data > terminating the client from its open handler while handshake bytes are buffered shuts down cleanly [595.38ms]
5 pass
0 fail
6 expect() calls
Ran 5 tests across 1 file. [8.24s]
__F:0:S:0
release with fix: all passed
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped) in 7283ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[1/2355] host-cxx deps/icu/host-obj/source/i18n/reldtfmt.cpp.o
[2/2355] host-cxx deps/icu/host-obj/source/i18n/regexcmp.cpp.o
[3/2355] host-cxx deps/icu/host-obj/source/i18n/regeximp.cpp.o
[4/2355] host-cxx deps/icu/host-obj/source/i18n/regexst.cpp.o
[5/2355] host-cxx deps/icu/host-obj/source/i18n/regextxt.cpp.o
[6/2355] host-cxx deps/icu/host-obj/source/i18n/remtrans.cpp.o
[7/2355] host-cxx deps/icu/host-obj/source/i18n/rematch.cpp.o
[8/2355] host-cxx deps/icu/host-obj/source/i18n/rbnf.cpp.o
[9/2355] host-cxx deps/icu/host-obj/source/i18n/region.cpp.o
[10/2355] host-cxx deps/icu/host-obj/source/i18n/rbtz.cpp.o
[11/2355] host-cxx deps/icu/host-obj/source/i18n/reldatefmt.cpp.o
[12/2355] host-cxx deps/icu/host-obj/source/i18n/repattrn.cpp.o
[13/2355] host-cxx deps/icu/host-obj/source/i18n/selfmt.cpp.o
[14/2355] host-cxx deps/icu/host-obj/source/i18n/scriptset.cpp.o
[15/2355] host-cxx deps/icu/host-obj/source/i18n/scientificnumberformatter.cpp.o
[16/2355] host-cxx deps/icu/host-obj/source/i18n/sharedbreakite
... (truncated)
diff hotspot
.../websocket_client/WebSocketUpgradeClient.rs | 3 ++
.../websocket/websocket-client-short-read.test.ts | 59 ++++++++++++++++++++++
2 files changed, 62 insertions(+)
gate history · 1 passed · 0 rejected · iteration 1
evidence per changed file
file reads edits tests
src/http_jsc/websocket_client/WebSocketUpgradeClient.rs 5 3 5
…st/js/web/websocket/websocket-client-short-read.test.ts 2 3 5