Skip to content

Bun.serve: cap chunk-extension bytes at 16 KiB per chunk - #34504

Merged
cirospaciari merged 4 commits into
mainfrom
claude/f52ce02d/chunk-extension-cap
Jul 17, 2026
Merged

Bun.serve: cap chunk-extension bytes at 16 KiB per chunk#34504
cirospaciari merged 4 commits into
mainfrom
claude/f52ce02d/chunk-extension-cap

Conversation

@robobun

@robobun robobun commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator

Repro

// POST with Transfer-Encoding: chunked whose chunk-size line carries 32 MiB
// of extension bytes. Body is 2 bytes; maxRequestBodySize is 1 MiB.
const S = Bun.serve({
  port: 0, hostname: "127.0.0.1", maxRequestBodySize: 1024 * 1024,
  async fetch(req) { const n = (await req.arrayBuffer()).byteLength; return new Response(`n=${n}`); },
});
// raw socket client writes: headers, then "2;" + 32 MiB of 'e', then "\r\nhi\r\n0\r\n\r\n"
// before:  ext-bytes-accepted=33554432 status=200
// after:   status=413, connection closed

A hostile client can hold a connection, a parser, and unmetered inbound bandwidth with bytes that maxRequestBodySize never sees: the body is 2 bytes, the limit is 1 MiB, yet 32 MiB+ of chunk-extension garbage flows and the request completes 200. idleTimeout never fires while extension bytes trickle. Node (llhttp) and Bun's own node:http server reject the same request with 413/400 at ~16 KiB.

Cause

HttpParser::MAX_CHUNK_EXTENSION_SIZE (16 KiB, matching Node's kMaxChunkExtensionsSize) already existed and the parser already counted extension bytes, but every overflow check was gated on the IsNodeHttp template parameter and the per-connection counter lived only in the HttpResponseData<SSL, true> (node:http) specialization; the Bun.serve instantiation passed nullptr and its template contained no use of the counter.

Fix

Move chunkedExtensionsByteCount into the base HttpResponseData<SSL, false> so it exists for both server personalities, pass it unconditionally from HttpContext, and drop the IsNodeHttp && gate on the six overflow checks and the per-request reset in HttpParser.h. Bun.serve now answers 413 Payload Too Large and closes the connection when a single chunk's extension bytes exceed 16 KiB, matching node:http and llhttp.

The cap is per chunk (reset in consumeHexNumber at the start of each chunk-size line, same as llhttp's on_chunk_header), not per message; a second test covers 10 chunks x 8 KiB extension = 80 KiB total still being accepted.

Verification

# without the fix (src/ + packages/ stashed):
(fail) rejects chunk extensions that exceed the 16 KiB per-chunk cap
  Expected to not contain: "200"
  Received: "HTTP/1.1 200 OK ... n=2"

# with the fix:
(pass) rejects chunk extensions that exceed the 16 KiB per-chunk cap
(pass) accepts small chunk extensions on every chunk (cap is per chunk, not per message)

Also green: test/js/bun/http/request-smuggling.test.ts (75 pass), test/js/node/test/parallel/test-http-chunked-smuggling.js, test/js/node/http/node-http-server-timeouts.test.ts.


[review] gate passed · iteration 1 · 4 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/mechgate.xml" test/js/bun/http/http-server-chunking.test.ts
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: checking for self-update (current version: 1.29.0)
bun test v1.4.0 (2fc4246ac)

test/js/bun/http/http-server-chunking.test.ts:
(pass) HTTP server handles chunked transfer encoding > handles fragmented chunk terminators [585.66ms]
(pass) HTTP server handles chunked transfer encoding > rejects invalid terminator in fragmented reads [517.84ms]
(pass) HTTP server handles split chunk-size CRLF > handles lone CR at end of chunk-size line across TCP segments [640.62ms]
(pass) HTTP server handles split chunk-size CRLF > handles lone CR in chunk-size with extensions [665.61ms]
269 | 
270 |     expect(stderr).toBe("");
271 |     const { received } = JSON.parse(stdout);
272 |     // Server must reject (413) and close the connection; it must not hand the
273 |     // request to fetch() (which would 
... (truncated)

release without fix: all passed
bun test v1.4.0-canary.1 (8342a4e5f)

test/js/bun/http/http-server-chunking.test.ts:
(pass) HTTP server handles split chunk-size CRLF > rejects chunk extensions that exceed the 16 KiB per-chunk cap [19.39ms]
(pass) HTTP server handles split chunk-size CRLF > accepts small chunk extensions on every chunk (cap is per chunk, not per message) [18.34ms]
(pass) HTTP server handles split chunk-size CRLF > rejects bare LF in chunk-size position (invalid byte not stranded) [17.29ms]
(pass) HTTP server handles split chunk-size CRLF > rejects chunk-size with zero hex digits (bare CRLF) > rejects and does not process trailing pipelined request [15.31ms]
(pass) HTTP server handles split chunk-size CRLF > rejects chunk-size with zero hex digits (extension only) > rejects and does not process trailing pipelined request [14.23ms]
(pass) HTTP server handles split chunk-size CRLF > rejects Content-Length values that would alias chunked-encoding state bits [32.01ms]
(pass) HTTP server handles split chunk-size CRLF > rejects chunk-size with zero hex digits (bare CRLF) > rejects when chunk-size line is split across packets [52.65ms]
(pass) HTTP server handles chunked transfer encoding >
... (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/mechgate.xml" test/js/bun/http/http-server-chunking.test.ts
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: checking for self-update (current version: 1.29.0)
bun test v1.4.0 (2fc4246ac)

test/js/bun/http/http-server-chunking.test.ts:
(pass) HTTP server handles chunked transfer encoding > handles fragmented chunk terminators [563.52ms]
(pass) HTTP server handles chunked transfer encoding > rejects invalid terminator in fragmented reads [567.56ms]
(pass) HTTP server handles split chunk-size CRLF > handles lone CR at end of chunk-size line across TCP segments [620.06ms]
(pass) HTTP server handles split chunk-size CRLF > handles lone CR in chunk-size with extensions [678.40ms]
(pass) HTTP server handles split chunk-size CRLF > rejects chunk extensions that exceed the 16 KiB per-chunk cap [529.28ms]
(pass) HTTP server handles split chunk-size CRLF > accepts small chunk extensions on every chunk (c
... (truncated)

release with fix: all passed
$ bun scripts/build.ts --profile=release
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: checking for self-update (current version: 1.29.0)
[configured] bun-profile → bun (stripped) in 734ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[1/23] gen JS modules (bundle-modules)
Preprocess modules (7463ms)
Bundle modules (36ms)
Postprocesss modules (161ms)
Bundle Functions (779ms)
Generate Code (81ms)

[8.54s] Bundled "src/js" for production
  2035 kb
  165 internal modules
  13 native modules
  90 internal functions across 19 files
[1/12] cargo bun_bin → libbun_rust.a (--target x86_64-unknown-linux-gnu)
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: component rust-std is up to date

  nightly-2026-05-06-x86_64-unknown-linux-gnu unchanged - rustc 1.97.0-nightly (e95e73209 2026-05-05)

info: checking for self-update (current versio
... (truncated)
diff hotspot
packages/bun-uws/src/HttpContext.h            | 10 ++-
 packages/bun-uws/src/HttpParser.h             | 26 ++++----
 packages/bun-uws/src/HttpResponseData.h       | 10 +--
 test/js/bun/http/http-server-chunking.test.ts | 93 ++++++++++++++++++++++++++-
 4 files changed, 113 insertions(+), 26 deletions(-)

gate history · 1 passed · 1 rejected · iteration 1

evidence per changed file
file                                           reads  edits  tests
packages/bun-uws/src/HttpContext.h                 1      1      0
packages/bun-uws/src/HttpParser.h                  2      7      0
packages/bun-uws/src/HttpResponseData.h            3      2      0
test/js/bun/http/http-server-chunking.test.ts      4      5      0

The parser already counted chunk-extension bytes and capped them at
MAX_CHUNK_EXTENSION_SIZE (16 KiB, matching Node/llhttp), but the check
and the per-connection counter were gated to the node:http personality.
A Bun.serve server therefore accepted unbounded chunk-extension bytes:
a client could stream hundreds of MB of extension garbage inside one
request (2-byte body, 200 OK) that maxRequestBodySize never sees.

Move the counter into the base HttpResponseData so it exists for both
server personalities, pass it unconditionally from HttpContext, and
remove the IsNodeHttp gate on the six overflow checks and the per-
request reset. Bun.serve now answers 413 and closes the connection the
same way node:http (and every llhttp-based peer) already did.
@robobun

robobun commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator Author

Status: reproduced (32 MiB extension bytes accepted, 200 OK), fixed (413 + close), fail-before/pass-after verified. PR #34504.

CI build 74724: 285/286 jobs passed; the one red lane is test-worker-message-port-transfer-terminate.js on debian x64-asan, a pre-existing JSC assertion failure on main unrelated to this diff. http-server-chunking.test.ts and request-smuggling.test.ts are green on every lane.

@robobun

robobun commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 3:34 PM PT - Jul 17th, 2026

@robobun, your commit 2fc4246 has 1 failures in Build #74724 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 34504

That installs a local version of the PR into your bun-34504 executable, so you can run:

bun-34504 --bun

@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Changes

Chunk-extension byte tracking is shared across HTTP server personalities, and the parser now enforces a 16 KiB per-chunk extension limit. Regression tests cover oversized-extension rejection, smaller extensions across multiple chunks, and fragmented requests.

Chunk extension enforcement

Layer / File(s) Summary
Shared chunk-extension state
packages/bun-uws/src/HttpResponseData.h, packages/bun-uws/src/HttpContext.h
Moves chunkedExtensionsByteCount into shared response state and passes it through chunk consumption for all server personalities.
Parser limit enforcement
packages/bun-uws/src/HttpParser.h
Documents the general limit, resets the per-chunk counter, and rejects extensions exceeding 16 KiB without IsNodeHttp gating.
Chunking regression coverage
test/js/bun/http/http-server-chunking.test.ts
Tests HTTP 413 rejection, HTTP 200 acceptance across multiple smaller extensions, and fragmented requests using 127.0.0.1.

Possibly related PRs

Suggested reviewers: jarred-sumner

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed Clearly states the main change: capping Bun.serve chunk-extension bytes at 16 KiB per chunk.
Description check ✅ Passed It covers the PR purpose, fix, and verification, though it uses custom headings instead of the template's exact sections.

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@test/js/bun/http/http-server-chunking.test.ts`:
- Around line 253-256: Update the chunking tests around the existing
extension-byte cases to cover the exact 16 KiB boundary: add an accepted chunk
with exactly 16 KiB of extensions and a rejected case with 16 KiB plus one byte,
while retaining multiple accepted chunks to verify the limit applies per chunk
rather than to the whole message.
- Line 259: Update both error handlers in the fixtures at
test/js/bun/http/http-server-chunking.test.ts lines 259-259 and 303-303 to
retain the reject function from Promise.withResolvers() and invoke it with the
socket failure; ensure both fixture promises reject immediately on error instead
of discarding failures.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 9b0ed5bd-91f2-4d04-ba15-f2a2bc63705a

📥 Commits

Reviewing files that changed from the base of the PR and between a215285 and 1ea3d9c.

📒 Files selected for processing (4)
  • packages/bun-uws/src/HttpContext.h
  • packages/bun-uws/src/HttpParser.h
  • packages/bun-uws/src/HttpResponseData.h
  • test/js/bun/http/http-server-chunking.test.ts

Comment thread test/js/bun/http/http-server-chunking.test.ts Outdated
Comment thread test/js/bun/http/http-server-chunking.test.ts
Comment thread packages/bun-uws/src/HttpParser.h
The HTTP_PARSER_ERROR_CHUNK_EXTENSIONS_OVERFLOW doc comment still said
'node:http compat only' but the cap now applies to every server
personality.

The tiny-send-buffer test used hostname 'localhost' for Bun.serve, which
on hosts where 'localhost' resolves to ::1 binds IPv6 only while the
Bun.connect side tries IPv4, giving ECONNREFUSED. 127.0.0.1 is
unambiguous.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

No issues found — the change is a clean, mechanical un-gating of the existing node:http chunk-extension cap so it also applies to Bun.serve, and the stale enum comment I flagged earlier is now fixed. Deferring only because this alters Bun.serve's request-acceptance behavior in the HTTP parser hot path (previously-accepted >16 KiB chunk-extension lines now 413), which is worth a human sign-off.

What was reviewed

  • Verified chunkedExtensionsByteCount is now always passed non-null from the sole consumePostPadded caller, so the newly-unconditional dereferences are safe.
  • Confirmed consumeHexNumber in ChunkedEncoding.h already null-checks the counter and resets it per chunk-size line, matching the "per chunk, not per message" contract the accept-test covers.
  • Checked the overflow check runs before each chunk dispatch, so no body bytes reach fetch() on the reject path; the 413 is written by the existing httpErrorResponses path.
Extended reasoning...

Overview

The PR moves the chunkedExtensionsByteCount field from the node:http-only HttpResponseData<SSL, true> specialization into the shared base, passes it unconditionally from HttpContext::onData, and drops the IsNodeHttp && gate on the six overflow checks and the per-request reset in HttpParser.h. Two new tests in http-server-chunking.test.ts cover reject (20 KiB → 413) and accept (10×8 KiB per-chunk → 200), plus a drive-by pin of an existing test to 127.0.0.1. The stale "node:http compat only" comment on the HTTP_PARSER_ERROR_CHUNK_EXTENSIONS_OVERFLOW enum value that I flagged in a prior run was fixed in 2fc4246.

Security risks

This is itself a security hardening: it closes an unmetered-bandwidth / connection-holding vector where a hostile client could stream unbounded chunk-extension bytes past maxRequestBodySize. The counter, cap constant, and error path already existed and shipped for node:http servers; this PR only enables them for the Bun.serve template instantiation. I verified there is exactly one caller of consumePostPadded (HttpContext.h:340) and it now always passes a non-null counter pointer, so the newly-unconditional *chunkedExtensionsByteCount dereferences cannot fault. consumeHexNumber still null-checks its own pointer parameter, so any future caller passing nullptr remains safe at that layer. No new parsing logic is introduced, so there is no new smuggling/desync surface.

Level of scrutiny

High — packages/bun-uws/src/HttpParser.h is the request-line/body parser for every Bun.serve connection, and this change makes previously-accepted requests (>16 KiB of chunk-extension bytes on one chunk-size line) now return 413 and close. That is almost certainly desirable and matches Node/llhttp, but it is a user-visible behavior change to Bun's native HTTP server default, which per repo guidance is the kind of change a maintainer should explicitly sign off on rather than have auto-approved.

Other factors

All prior review threads are resolved: CodeRabbit's boundary-test suggestion was addressed (reject case tightened to 20 KiB in 8342a4e with a reasoned explanation for not pinning the exact byte, since the counter includes the leading ;), CodeRabbit's error-handler suggestion was declined as matching file convention (8 existing empty error() {} handlers in the same file, with close() guaranteed after error()), and my own stale-comment nit was fixed. The PR carries fail-before/pass-after evidence on both debug+ASAN and release builds, and the surrounding chunking/smuggling suites stayed green. The new tests' expect(stderr).toBe("") pattern matches six pre-existing instances in the same file. The 8-byte per-connection cost added to the Bun.serve HttpResponseData layout is negligible.

@cirospaciari
cirospaciari enabled auto-merge (squash) July 17, 2026 22:37
@cirospaciari
cirospaciari disabled auto-merge July 17, 2026 22:37
@cirospaciari
cirospaciari merged commit 39c70a3 into main Jul 17, 2026
77 of 78 checks passed
@cirospaciari
cirospaciari deleted the claude/f52ce02d/chunk-extension-cap branch July 17, 2026 22:37
robobun added a commit that referenced this pull request Jul 17, 2026
…eview

- resolve HttpContext.h conflict with #34504: keep useLenientTransferEncoding
  and use the now-base-struct chunkedExtensionsByteCount
- fallback: hoist onHttp1SocketEnd so the Upgrade/CONNECT handoff removes it
  before closing the parser
- clear_on_data_callback: prefer armed_this_value over the socket-current
  wrapper when clearing the cached ondata slot
- connectionListener test: reject on error/close for a useful failure message
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants