Skip to content

Hardening: input validation and compatibility fixes across 9 subsystems (round 9)#31606

Merged
Jarred-Sumner merged 21 commits into
mainfrom
claude/security-round-10
Jun 2, 2026
Merged

Hardening: input validation and compatibility fixes across 9 subsystems (round 9)#31606
Jarred-Sumner merged 21 commits into
mainfrom
claude/security-round-10

Conversation

@Jarred-Sumner

Copy link
Copy Markdown
Collaborator

Tightens input validation, pointer/lifetime handling, and Node-compatibility across the package manager, bun-lambda, node:zlib/net/http, Glob, the transpiler cache, postgres, and HTMLRewriter. Continuation of #31417, #31495, and #31559; same structure — small per-area commits with regression tests included.

Scaffolding / Lambda

  • create: dependency names detected from scanned source files are passed to the spawned installer as positionals (after --), so a specifier can never be interpreted as a flag
  • bun-lambda: event-to-Request conversion builds the URL from the request context's domain; client-supplied path bytes can no longer change the URL authority

node runtime

  • node:zlib: native compression handles re-resolve the caller-supplied output array on every write completion instead of caching a raw pointer, and reject detached/resized buffers
  • node:net: BlockList subnet matching handles IPv4-mapped IPv6 rules the same way Node does (verified against Node's behavior)
  • node:http: duplicate request headers follow Node's IncomingMessage precedence — first value wins for singleton headers, others are joined, set-cookie is arrayed (verified against Node's behavior)
  • Glob: an oversized cwd option throws a catchable error instead of aborting the process

Runtime internals

  • transpiler: cached module metadata records with out-of-range string indices are rejected at load (previously a corrupted cache record could crash the process)
  • postgres: unknown backend messages are skipped by exactly their declared length, and NotificationResponse is consumed through the protocol decoder, so the stream can no longer desynchronize
  • HTMLRewriter: comment.replace() removes the original comment and emits only the replacement (previously both appeared in the output)

Not changed

The bin-link containment behavior for symlinked targets is unchanged (previously decided); the recursive-delete traversal change is already covered by the open #31337.

Tests

11 regression tests across the touched areas. Most are validated to fail on the released build and pass here — including one where the released build segfaults on a corrupted cache record and one where it hangs on an oversized Glob cwd. The zlib write-state tests are regression-only (the released build does not exhibit the failure deterministically without ASAN).

cargo check and the cross-target checks pass on all 10 CI targets; the touched suites pass locally with no new failures versus main.

@robobun

robobun commented May 30, 2026

Copy link
Copy Markdown
Collaborator
Updated 2:05 PM PT - Jun 2nd, 2026

@robobun, your commit 054a9fc has some failures in Build #59982 (All Failures)


🧪   To try this PR locally:

bunx bun-pr 31606

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

bun-31606 --bun

@coderabbitai

coderabbitai Bot commented May 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@robobun, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 28 minutes and 24 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 8bd3ff20-90fe-4f04-b7d1-288ae184628a

📥 Commits

Reviewing files that changed from the base of the PR and between 3e0ef3d and 054a9fc.

📒 Files selected for processing (3)
  • scripts/verify-baseline-static/src/main.rs
  • src/jsc/bindings/NodeHTTP.cpp
  • test/js/node/http/node-http.test.ts

Walkthrough

Hardens request authority and input validation, adds duplicate-header semantics and Postgres notification handling, refactors zlib write-state caching to JS-backed properties, improves BlockList subnet matching, and expands tests for these edge cases.

Changes

Lambda Security, PostgreSQL, Node.js, and Input Validation

Layer / File(s) Summary
Lambda request URL construction security
packages/bun-lambda/runtime.ts
Prefer request.domainName over Host and build URLs from a base ${proto}://${hostname}/ then set pathname for HTTP v1/v2 and WebSocket upgrade events.
HTTP duplicate header handling and Set-Cookie array
src/jsc/bindings/NodeHTTP.cpp, test/js/node/http/node-http.test.ts
Detect duplicates during uWS header assignment and rebuild headers object with Node.js semantics: Set-Cookie becomes an array, Cookie joined with "; ", single-value headers keep first value, others comma-join.
PostgreSQL NotificationResponse message support
src/sql_jsc/postgres/PostgresRequest.rs, src/sql_jsc/postgres/PostgresSQLConnection.rs, test/js/sql/postgres-multi-statement-fields.test.ts
Adds MessageType::NotificationResponse (b'A'), dispatches to it, decodes/logs unsupported notifications, and validates unknown-message lengths before skipping.
Node.js BlockList IPv4/IPv6 subnet matching
src/runtime/node/net/BlockList.rs, test/js/node/net/node-net.test.ts
Refactors subnet checks: IPv4 uses as_sin() mapping and /0 fast-path; IPv6 compares as u128 and supports IPv4-mapped IPv6 comparisons.
Input validation and bounds checking
src/bun_core/util.rs, src/bundler_jsc/analyze_jsc.rs, src/runtime/api/glob.rs, test/js/bun/glob/scan.test.ts
Adds empty-slice short-circuit in alignment casts, bounds-checks StringID indices before use in bundler, and rejects oversized cwd early in glob parsing.
Zlib write-state: pointer to JSValue caching
src/runtime/api/zlib.classes.ts, src/runtime/node/node_zlib_binding.rs, src/runtime/node/zlib/NativeBrotli.rs, src/runtime/node/zlib/NativeZlib.rs, src/runtime/node/zlib/NativeZstd.rs, test/js/node/zlib/zlib-handle-bounds-check.test.ts
Removes native *mut u32 write_result fields; caches writeResult on JS this via generated accessors; flush_write_result re-resolves and validates the backing ArrayBuffer per call.
HTML comment binding and project generation CLI fixes
src/lolhtml_sys/lol_html.rs, src/runtime/cli/create/SourceFileProjectGenerator.rs, test/cli/create/create-jsx.test.ts
Adds lol_html_comment_replace FFI and updates Comment::replace; inserts explicit -- separator into generated bun CLI argv for dependency/component positionals.
Comprehensive test coverage
test/integration/bun-lambda/bun-lambda.test.ts, test/cli/run/transpiler-cache.test.ts, various test/js/*
Adds integration and regression tests for Lambda authority safety, transpiler-cache corruption rejection, oversized cwd handling, duplicate-header precedence, BlockList subnet rules, zlib writeState resilience, Postgres framing with async messages, CLI auto-install args, and HTMLRewriter comment mutations.
  • Possibly related PRs:

    • oven-sh/bun#30975: Changes in src/bundler_jsc/analyze_jsc.rs touching module-record handling; relevant to string-id/identifier validation.
  • Suggested reviewers:

    • cirospaciari
    • pfgithub
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Hardening: input validation and compatibility fixes across 9 subsystems (round 9)' accurately summarizes the PR's main focus on security and compatibility improvements across multiple areas, though it is somewhat broad in scope.
Description check ✅ Passed The PR description comprehensively covers what the changes do, lists affected subsystems with concrete examples, references related PRs, explains the approach, and includes verification details. It substantially exceeds the template requirements despite the template being minimal.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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

@github-actions

Copy link
Copy Markdown
Contributor

Found 3 issues this PR may fix:

  1. Unexpected handling of duplicate headers in http server #19372 - PR implements Node.js IncomingMessage duplicate header precedence rules (singleton headers keep first value, others joined with ", ", Cookie with "; ", Set-Cookie as array), directly fixing the reported incorrect handling of duplicate headers in http server
  2. sql: support postgresql notify/listen #18214 - PR adds NotificationResponse (message type 'A') handling to the Postgres protocol decoder, which is a prerequisite for LISTEN/NOTIFY support
  3. Postgres send error randomly when run heavy integration tests #24326 - PR fixes Postgres unknown message skip length (was off by 3 bytes, causing stream desynchronization) and adds minimum length validation, which would cause random protocol errors under heavy load

If this is helpful, copy the block below into the PR description to auto-close these issues on merge.

Fixes #19372
Fixes #18214
Fixes #24326

🤖 Generated with Claude Code

@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/cli/run/transpiler-cache.test.ts`:
- Around line 351-355: The test asserts first.exitCode before other expectations
which reduces failure context; update the run block using the variable first
(from run()) so that you check stderr/stdout and existsSync(cache_dir) first and
assert expect(first.exitCode).toBe(0) last, i.e., move the exit code assertion
for the 'first' run to the end of that block after the other expects.

In `@test/js/node/zlib/zlib-handle-bounds-check.test.ts`:
- Around line 143-145: The test currently only awaits proc.stdout.text() and
proc.exited and asserts stdout and exitCode separately, which can hide crash
diagnostics; update the await to capture stderr as well (e.g., await
Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited])) and replace
the separate expects with a single assertion on the combined result object
(include stdout.trim(), stderr.trim(), and exitCode) so stdout/stderr/exit are
all visible together when the subprocess fails; locate and change the variables
used in this test (proc, stdout, stderr, exitCode) and the two expect calls to
assert the combined object.
🪄 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: 172b0cc1-c23d-49a1-a29e-0f4feaeac6bf

📥 Commits

Reviewing files that changed from the base of the PR and between 7069b30 and 2f94449.

⛔ Files ignored due to path filters (1)
  • test/cli/create/__snapshots__/create-jsx.test.ts.snap is excluded by !**/*.snap
📒 Files selected for processing (24)
  • packages/bun-lambda/runtime.ts
  • src/bun_core/util.rs
  • src/bundler_jsc/analyze_jsc.rs
  • src/jsc/bindings/NodeHTTP.cpp
  • src/lolhtml_sys/lol_html.rs
  • src/runtime/api/glob.rs
  • src/runtime/api/zlib.classes.ts
  • src/runtime/cli/create/SourceFileProjectGenerator.rs
  • src/runtime/node/net/BlockList.rs
  • src/runtime/node/node_zlib_binding.rs
  • src/runtime/node/zlib/NativeBrotli.rs
  • src/runtime/node/zlib/NativeZlib.rs
  • src/runtime/node/zlib/NativeZstd.rs
  • src/sql_jsc/postgres/PostgresRequest.rs
  • src/sql_jsc/postgres/PostgresSQLConnection.rs
  • test/cli/create/create-jsx.test.ts
  • test/cli/run/transpiler-cache.test.ts
  • test/integration/bun-lambda/bun-lambda.test.ts
  • test/js/bun/glob/scan.test.ts
  • test/js/node/http/node-http.test.ts
  • test/js/node/net/node-net.test.ts
  • test/js/node/zlib/zlib-handle-bounds-check.test.ts
  • test/js/sql/postgres-multi-statement-fields.test.ts
  • test/js/workerd/html-rewriter.test.js

Comment thread test/cli/run/transpiler-cache.test.ts
Comment thread test/js/node/zlib/zlib-handle-bounds-check.test.ts Outdated

@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.

I didn't find any correctness issues, but this touches memory-safety and security-sensitive paths across nine subsystems (zlib pointer lifetime, NodeHTTP.cpp header handling, lambda URL authority, argv injection), so it warrants a human review.

Extended reasoning...

Overview

This PR makes hardening/compatibility changes across 9 distinct subsystems (25 files): bun-lambda URL construction, bun create argv building, node:zlib native handle write-state pointer handling, node:net BlockList IPv4-mapped-IPv6 subnet matching, node:http duplicate-header precedence (~110 lines of new C++ in NodeHTTP.cpp), Glob cwd length validation, transpiler-cache module-record index validation, Postgres protocol message framing, and HTMLRewriter comment.replace(). Each change ships with a regression test.

Security risks

Several changes are explicitly security-motivated: the lambda runtime now derives the request authority from requestContext.domainName and assigns the path via url.pathname so a //attacker.example/... path can't rewrite the origin; bun create inserts -- before scanned dependency names so a specifier can't be parsed as an installer flag; the zlib refactor removes a cached raw *mut u32 into a JS-owned ArrayBuffer in favor of re-resolving the typed array on each completion; and the transpiler-cache deserializer rejects out-of-range string indices that previously crashed the process. These look correct to me, but they sit on attack-surface boundaries and merit a second pair of eyes.

Level of scrutiny

High. The zlib change rewires pointer lifetime across three native handle types plus the codegen values: list; NodeHTTP.cpp adds a new slow-path buildHeadersObjectHandlingDuplicates with non-trivial JSC object/identifier handling (including the parseIndex numeric-header edge case); and the BlockList change alters IP-matching semantics. None of these are mechanical.

Other factors

The bug-hunting pass surfaced only a test-convention nit (explicit per-test timeouts), which doesn't affect correctness. CI shows a pre-existing -no-pie linker warning unrelated to this diff. Given the breadth and the security-adjacent nature of several fixes, I'm deferring rather than approving.

Comment thread test/cli/run/transpiler-cache.test.ts Outdated
Comment thread src/jsc/bindings/NodeHTTP.cpp Outdated
Comment thread src/jsc/bindings/NodeHTTP.cpp Outdated
Comment thread src/jsc/bindings/NodeHTTP.cpp Outdated

@Jarred-Sumner Jarred-Sumner left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Can you reach into the internals of PutPropertySlot to see where it finds out if there's an existing property there and then if so, we do the right thing - avoiding the extra property lookup in the rare case?

@Jarred-Sumner

Copy link
Copy Markdown
Collaborator Author

@robobun Can you reach into the internals of PutPropertySlot to see where it finds out if there's an existing property there and then if so, we do the right thing - avoiding the extra property lookup in the rare case?

@robobun

robobun commented Jun 2, 2026

Copy link
Copy Markdown
Collaborator

✅ Reworked per feedback in 054a9fc.

  • No put-before-check: each header name is looked up first with getDirectOffset — the same structure()->get the PutPropertySlot replace path uses — and only stored if absent. On a duplicate the first value is still intact at the returned offset: singleton headers keep it with no write at all, joinable/cookie headers merge through didReplaceProperty + putDirectOffset at that offset (no second lookup). This also removed the raw-header-list rescan entirely — the old value is simply read at the offset.
  • No ropes / no temporary JSStrings: merged values are built with tryMakeString(existing, separator, value) as one flat 8-bit string; the index-name path's rope + temporary separator JSString are gone.

Tests (incl. triples, mixed-case, empty-first-value, server/retry-after singletons, numeric names — expectations verified against Node v24) pass with the rework and fail on the released build.


CI context from the previous rounds: all Linux/Windows/ASAN lanes green across three builds; node-http.test.ts also passed 85/85 on the darwin-26 shard. Every red job was diagnosed as macOS-agent Docker provisioning (missing bun_sql_test Postgres roles, squid never starting, legacy Docker without --progress, MinIO 507 disk-full) — details in the edit history.

…ot offset

When putDirect reports ExistingProperty, the slot has already recorded the
property's offset, so the duplicate can be fixed up with putDirectOffset at
that offset instead of flagging the request and rebuilding the whole headers
object with a second pass of property lookups. The merged value is re-derived
from the raw header list, which also handles names that differ only by case.
Index-shaped names, which bypass PutPropertySlot, check getDirectIndex
directly instead of forcing a rebuild.

@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, but this touches nine subsystems including low-level JSC property-slot internals in NodeHTTP.cpp, memory-lifetime changes in the zlib write-state path, and security-relevant Lambda URL/argv handling — worth a human pass, and the macOS x64 sql.test.ts / sql-prepare-false.test.ts failures may be related to the Postgres framing changes.

Extended reasoning...

Overview

This PR makes hardening and Node-compat fixes across 26 files in nine independent subsystems: bun-lambda request-URL construction, bun create argv building, node:zlib write-state lifetime, node:net BlockList IPv4-mapped matching, node:http duplicate-header merging in NodeHTTP.cpp, Glob cwd validation, transpiler-cache module-record bounds checking, Postgres unknown-message framing + NotificationResponse handling, and HTMLRewriter comment.replace. Each area ships with a regression test.

Security risks

Several changes are explicitly security-relevant: the Lambda runtime now derives the request authority from requestContext.domainName and assigns pathname separately so a client-supplied path can't rewrite the origin; SourceFileProjectGenerator inserts -- before scanned dependency names so a specifier can't be parsed as a CLI flag; the zlib handles stop caching a raw *mut u32 into a JS-owned ArrayBuffer and re-resolve it per write; and the bundler rejects out-of-range string indices in cached module records. These all narrow attack surface rather than widen it, but they are exactly the kind of changes that benefit from a second set of eyes.

Level of scrutiny

High. The NodeHTTP.cpp change went through three iterations during review and now relies on PutPropertySlot::ExistingProperty + putDirectOffset / structure()->didReplaceProperty() to merge duplicate headers in place — subtle JSC internals on a hot path. The zlib change removes a native pointer field across three codegen-backed classes. The Postgres change alters wire-protocol framing for unknown messages. None of these are mechanical.

Other factors

The author has already self-reviewed and addressed all inline comments (CodeRabbit's two nits and my earlier note about per-test timeouts were fixed in e005625; the NodeHTTP.cpp structure concerns were resolved in 3e0ef3d and 1ffc4ca). The bug-hunting system found nothing. However, CI on the latest commit shows sql.test.ts and sql-prepare-false.test.ts failing on macOS 14 x64, which overlaps the Postgres protocol changes here and should be confirmed as pre-existing flakes before merge. Given the breadth, the JSC-internals work, and the open SQL failures, I'm deferring rather than approving.

@Jarred-Sumner

Copy link
Copy Markdown
Collaborator Author

Still not correct. It shouldn't be creating temporary JSString (no rope). It should not call putDirect before checking if it exists (using the code path the PutPropertySlot uses).

…ings

Replace the put-then-fixup pattern: each header name is looked up with
getDirectOffset (the same structure lookup putDirect's replace path uses)
before anything is stored. On a duplicate the first value is still intact
at the returned offset, so singleton headers keep it with no write at all
and joinable/cookie headers merge through putDirectOffset. The merged
value is built with tryMakeString as a single flat string instead of a
rope, and the temporary separator JSString in the index-name path is gone.
@Jarred-Sumner

Copy link
Copy Markdown
Collaborator Author

Addressed in 054a9fc. The header loops no longer put before checking: each name's own-property offset is looked up first via getDirectOffset — the same structure()->get lookup putDirectInternal performs — and only a genuinely new property goes through putDirect. A valid offset means a duplicate, which merges in place with didReplaceProperty(offset) + putDirectOffset(vm, offset, merged) (the same order as putDirectInternal's existing-property path), never re-entering the generic put. Merged values are built as a single flat string with tryMakeString (existing value + separator + the raw value), producing exactly one flat JSString — the rope and the temporary ", " JSString in the index-shaped-name path are gone, and the per-duplicate rescan of the raw header list is removed. First-wins singleton duplicates skip the headers-object store entirely; the per-occurrence value JSString is kept only because every occurrence still lands in rawHeaders. Verified against the duplicate-header precedence tests in test/js/node/http/node-http.test.ts and numeric-header.test.ts, plus a BUN_JSC_validateExceptionChecks=1 run.

@Jarred-Sumner Jarred-Sumner merged commit 684f2af into main Jun 2, 2026
76 of 77 checks passed
@Jarred-Sumner Jarred-Sumner deleted the claude/security-round-10 branch June 2, 2026 20:25
Jarred-Sumner pushed a commit that referenced this pull request Jun 3, 2026
### What does this PR do?

Fixes #23149: a response that declares a `Content-Encoding` but sends
**zero body bytes** — e.g. an empty chunked gzip response — failed with
`ZlibError`. The decompressor ran on zero input at stream end and
reported a truncated stream; Node resolves these as an empty body.

```js
// server: HTTP/1.1 200 OK
//         Content-Encoding: gzip
//         Transfer-Encoding: chunked
//
//         0\r\n\r\n
const res = await fetch(url);
await res.text(); // before: throws ZlibError — after: ""
```

The fix is an early-out in `decompress_bytes`: when the response
delivered no body bytes at all (`buffer` empty and `total_body_received
== 0`), there is nothing to decompress. An empty final flush *after*
real data still flushes normally (`total_body_received` is non-zero on
that path).

### Tests

Two cases added to `test/js/web/fetch/fetch-gzip.test.ts` using a raw
socket server: empty gzip body via chunked encoding (fails on released
Bun with `ZlibError`, passes with this PR) and via `Content-Length: 0`
(already worked; pinned). `fetch-gzip.test.ts` + `fetch.test.ts` suites
pass locally (the only failures are pre-existing external-network tests
that fail identically on released Bun in this environment).


### CI notes

- The "multiple chunks, TCP server" test in the same file was pinned to
`127.0.0.1`: binding `localhost` can pick `::1` while `fetch()` resolves
`localhost` to `127.0.0.1`, which made it fail `ConnectionRefused` on
some hosts.
- `windows-x64-baseline-verify-baseline` failed with `strpbrk
[CLDEMOTE]` — newer UCRT on the CI Windows agents emits `cldemote` (`NP
0f 1c /0`, hint-space, NOP where unsupported) in `strpbrk`. An interim
per-symbol allowlist widening unblocked this branch; main then landed
the proper global ignore (#31606), so this branch merged main and
dropped the interim entry — no verifier changes remain in the diff.

---------

Co-authored-by: robobun <117481402+robobun@users.noreply.github.com>
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.

2 participants