Skip to content
Merged
Show file tree
Hide file tree
Changes from 100 commits
Commits
Show all changes
104 commits
Select commit Hold shift + click to select a range
650fe58
install: tighten dependency folder name validation
Jarred-Sumner May 22, 2026
bc24145
mysql: tighten handshake packet handling
Jarred-Sumner May 22, 2026
912ce20
router: fix decoded path buffer ownership
Jarred-Sumner May 22, 2026
1456ba5
structuredClone: bound constant pool index handling
Jarred-Sumner May 22, 2026
0d65ca4
install: align migration skip predicates
Jarred-Sumner May 22, 2026
885e7eb
node:http: tighten server socket receiver checks
Jarred-Sumner May 22, 2026
65955da
serve: fix ref accounting in deferred response path
Jarred-Sumner May 22, 2026
3792cf6
node:http: tighten parser execute handling
Jarred-Sumner May 22, 2026
620bfae
node:crypto: tighten cipher option handling
Jarred-Sumner May 22, 2026
67296c7
formdata: tighten header value parsing
Jarred-Sumner May 22, 2026
0afe9a4
install: copy manifest name before enqueue
Jarred-Sumner May 22, 2026
6b388f6
install: validate binary lockfile patch entries
Jarred-Sumner May 22, 2026
9b79959
fetch: tighten proxy response handling
Jarred-Sumner May 22, 2026
d34cb60
node:zlib: validate write state array length
Jarred-Sumner May 22, 2026
b82bb94
shell: track interpolated value ranges in parser
Jarred-Sumner May 22, 2026
8f87f6f
sql: validate transaction option strings
Jarred-Sumner May 22, 2026
4abfade
node:fs: pin read buffers for async operations
Jarred-Sumner May 22, 2026
ec7e7be
node:http: reorder buffered write conversion
Jarred-Sumner May 22, 2026
ebfd98c
uws: stop parsing when data handler invalidates socket
Jarred-Sumner May 22, 2026
af3500b
node:fs: snapshot vector buffers before deriving pointers
Jarred-Sumner May 22, 2026
72517ef
streams: re-derive pending view before copy
Jarred-Sumner May 22, 2026
5defeda
sql: tighten array type name validation
Jarred-Sumner May 22, 2026
15c5fa2
sqlite: revalidate statement around bind callbacks
Jarred-Sumner May 22, 2026
f3ad7bc
sourcemap: bound vlq decode loop
Jarred-Sumner May 22, 2026
d86503e
fetch: tighten content-length parsing
Jarred-Sumner May 22, 2026
921a1ae
url: bound query string parameter count
Jarred-Sumner May 22, 2026
78d3a53
install: require integrity for off-registry lockfile entries
Jarred-Sumner May 22, 2026
9955087
postgres: reject malformed array literals
Jarred-Sumner May 22, 2026
e2e3a26
node:zlib: hold write buffers across async work
Jarred-Sumner May 22, 2026
0bfc3e2
timers: clean up promoted timer map entries
Jarred-Sumner May 22, 2026
ecaa6cc
serve: cap streamed request body size
Jarred-Sumner May 22, 2026
2257eab
install: tighten symlink target normalization
Jarred-Sumner May 22, 2026
03a613d
resolver: validate export wildcard segments
Jarred-Sumner May 22, 2026
f9140f4
mysql: bound packet payload length
Jarred-Sumner May 22, 2026
149b4da
mysql: distinguish null marker from length prefix
Jarred-Sumner May 22, 2026
a088a65
blob: copy typed array parts during construction
Jarred-Sumner May 22, 2026
2031a01
http2: pass full name buffer to header validation
Jarred-Sumner May 22, 2026
56b9f2f
http2: bound headers frame padding
Jarred-Sumner May 22, 2026
7639b83
shell: terminate seq loop on non-advancing increment
Jarred-Sumner May 22, 2026
f39fbcb
tls: enforce renegotiation rate policy
Jarred-Sumner May 22, 2026
7700750
fetch: include host override in connection pool key
Jarred-Sumner May 22, 2026
cfa3ba6
strings: compare bytes in hashed string equality
Jarred-Sumner May 22, 2026
debf7c3
strings: avoid narrowing utf16 length conversion
Jarred-Sumner May 22, 2026
7e266df
node:crypto: reject unconvertible signature encodings
Jarred-Sumner May 22, 2026
5b2e1d1
node:vm: tighten module receiver checks
Jarred-Sumner May 22, 2026
260b872
fetch: reset host override on cross-origin redirect
Jarred-Sumner May 22, 2026
4360c91
dns: compare hostnames in cache lookup
Jarred-Sumner May 22, 2026
d3d692b
fs.watch: scope watcher manager to calling vm
Jarred-Sumner May 22, 2026
b294740
spawn: clear aliased buffer before source detach
Jarred-Sumner May 22, 2026
83b99df
glob: bound multi-byte literal comparison
Jarred-Sumner May 22, 2026
3e48f18
mysql: validate temporal value ranges
Jarred-Sumner May 22, 2026
1d30640
http2: validate response header values
Jarred-Sumner May 22, 2026
13c6be4
shell: open directories without following links during rm
Jarred-Sumner May 22, 2026
78e1ee1
structuredClone: validate view backing store tag
Jarred-Sumner May 22, 2026
957b4ff
websocket: enforce max payload on decompression fast path
Jarred-Sumner May 22, 2026
0d7eb62
node:fs: pin read buffers for async operations (extern decl consistency)
Jarred-Sumner May 22, 2026
5396bbb
[autofix.ci] apply automated fixes
autofix-ci[bot] May 22, 2026
64f4570
test: add regression coverage for input validation changes
Jarred-Sumner May 22, 2026
a0fd96f
node:http: surface unframeable responses as parse errors
Jarred-Sumner May 22, 2026
914eac5
blob: only copy typed array parts when a later part can run user code
Jarred-Sumner May 22, 2026
678971d
blob: also force part cloning while other entries remain to be walked
Jarred-Sumner May 22, 2026
5c2f428
fetch: handle proxy CONNECT responses consistently across redirect modes
Jarred-Sumner May 22, 2026
b53c8a7
install: align validation helpers across lockfile read/write paths
Jarred-Sumner May 22, 2026
ec04d7a
node:zlib: validate the writeResult argument type in init
Jarred-Sumner May 22, 2026
d72d7e4
url: bound the query-string pre-scan at the parameter cap
Jarred-Sumner May 22, 2026
5a0fd93
server: count pre-stream buffered bytes against the request body limit
Jarred-Sumner May 22, 2026
437f05a
fs.watch: free a displaced watcher manager on Windows
Jarred-Sumner May 22, 2026
4a209a5
sql: accept spaces inside array type modifiers
Jarred-Sumner May 22, 2026
d02b435
test: tighten assertions and follow string-building conventions
Jarred-Sumner May 22, 2026
9dc7f11
[autofix.ci] apply automated fixes
autofix-ci[bot] May 22, 2026
ec332c0
formdata: release the duplicated part content type after appending
Jarred-Sumner May 22, 2026
9870167
install: tighten folder name validation in the isolated linker
Jarred-Sumner May 23, 2026
1d91bfd
node:http: tighten parser execution state handling
Jarred-Sumner May 23, 2026
3fdfb95
sql: bound outgoing packet framing
Jarred-Sumner May 23, 2026
2121739
tls: bound peer-initiated renegotiation handling
Jarred-Sumner May 23, 2026
ac2ecf4
node:wasi: tighten path resolution against the preopened directory
Jarred-Sumner May 23, 2026
b63a91a
blob: tighten part buffer lifetime handling
Jarred-Sumner May 23, 2026
06eaeb0
[autofix.ci] apply automated fixes
autofix-ci[bot] May 23, 2026
ab3e7e8
tls: keep the safety comment adjacent to the renegotiate call
Jarred-Sumner May 23, 2026
b7d98b0
node:zlib: validate remaining typed array arguments in init
Jarred-Sumner May 23, 2026
97cd2d3
fetch: defer host override reset until the redirected socket is released
Jarred-Sumner May 23, 2026
6191839
test: use Buffer.alloc for repetitive payloads in deflate limit test
Jarred-Sumner May 23, 2026
a5a78e6
fetch: also drop the host override on multiplexed cross-origin redirects
Jarred-Sumner May 23, 2026
90427e7
node:fs: root collected vector elements for the duration of pointer d…
Jarred-Sumner May 23, 2026
ace4d31
remove redundant comments
Jarred-Sumner May 23, 2026
8816da7
fetch: distinguish content-length framing errors from malformed respo…
Jarred-Sumner May 23, 2026
0fa3e09
tls: reset the renegotiation count each window
Jarred-Sumner May 23, 2026
4dfb923
[autofix.ci] apply automated fixes
autofix-ci[bot] May 23, 2026
54526d8
fetch: also ignore transfer-encoding on a successful connect response
Jarred-Sumner May 23, 2026
7ba1407
wasi: walk up on any path resolution failure during the containment c…
Jarred-Sumner May 23, 2026
59e94e4
test: surface subprocess stderr in the remaining spawn assertions
Jarred-Sumner May 23, 2026
d812559
node: hold async I/O buffers in cached values instead of handles
Jarred-Sumner May 23, 2026
9fbd458
jsc: add ArrayBuffer::unpin and release async buffers on completion
Jarred-Sumner May 23, 2026
a6213b7
node:fs: gate vectored buffer arrays on the array structure, not its …
Jarred-Sumner May 23, 2026
7a238be
sqlite: always copy string parameters when binding
Jarred-Sumner May 23, 2026
1e0dae5
test: assert zlib write buffers are transferable after completion
Jarred-Sumner May 23, 2026
72a9754
node:fs: hold vectored I/O buffers for the duration of an async write
Jarred-Sumner May 23, 2026
a161f8d
node:fs: balance the exception check and buffer release on error paths
Jarred-Sumner May 23, 2026
004824f
test: assert empty stderr in two more subprocess tests
Jarred-Sumner May 23, 2026
749270b
test: assert empty stderr in the last two unchecked subprocess spawns
Jarred-Sumner May 23, 2026
339e101
formdata: accept HTAB inside part header values
Jarred-Sumner May 23, 2026
297d8d6
[autofix.ci] apply automated fixes
autofix-ci[bot] May 23, 2026
5c4345e
zlib: surface buffer materialization failure as an error
Jarred-Sumner May 23, 2026
f9bb063
test: build the oversized header name without String.repeat
Jarred-Sumner May 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions packages/bun-usockets/src/crypto/openssl.c
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
#include "libusockets.h"
#include <string.h>
#include <stdatomic.h>
#include <time.h>

/* These are in sni_tree.cpp */
void *sni_new();
Expand Down Expand Up @@ -799,7 +800,32 @@
}

static int ssl_renegotiate(struct us_socket_t *s) {
/* Server-forced renegotiation (HelloRequest -> SSL_ERROR_WANT_RENEGOTIATE).
* Enforce the per-context policy (default 3 per 600s, Node's
* CLIENT_RENEG_LIMIT/CLIENT_RENEG_WINDOW) before re-entering a full
* handshake — otherwise a malicious server can pin a core with
* back-to-back renegotiations. limit == 0 disables renegotiation; window
* == 0 means the per-connection counter never resets. Returning 0 makes
* the caller treat this as SSL_ERROR_SSL and close the connection. */
uint32_t limit, window;
us_reneg_policy(s_ssl(s), &limit, &window);
struct us_ssl_reneg_state_t *st = us_reneg_state(s_ssl(s));
s->ssl_handshake_state = HANDSHAKE_RENEGOTIATION_PENDING;
if (!st) {
ssl_trigger_handshake(s, 0);
return 0;
}
uint64_t now_ms = (uint64_t)time(NULL) * 1000;
if (st->count == 0 ||
(window && now_ms - st->window_start_ms >= (uint64_t)window * 1000)) {
st->window_start_ms = now_ms;
st->count = 0;
}

Check warning on line 823 in packages/bun-usockets/src/crypto/openssl.c

View check run for this annotation

Claude / Claude Code Review

openssl.c renegotiation window uses wall-clock time(NULL) instead of a monotonic clock

nit: this window uses wall-clock `time(NULL)`, so a backward clock step (NTP, manual `date -s`) makes the unsigned `now_ms - st->window_start_ms` underflow to ~2^64 ≥ `window*1000` and resets `st->count = 0` (fail-open). The Rust SSLWrapper path aligned to this one in resolved comment #3292012717 uses monotonic `std::time::Instant` for the same window — `clock_gettime(CLOCK_MONOTONIC)` here would match it. Not blocking: an attacker doesn't control the victim's clock, and pre-PR there was no limi
Comment thread
Jarred-Sumner marked this conversation as resolved.
if (st->count >= limit) {
ssl_trigger_handshake(s, 0);
return 0;
}
st->count++;
if (!SSL_renegotiate(s_ssl(s))) {
ssl_trigger_handshake(s, 0);
return 0;
Expand Down
28 changes: 23 additions & 5 deletions packages/bun-uws/src/HttpParser.h
Original file line number Diff line number Diff line change
Expand Up @@ -955,7 +955,12 @@ namespace uWS
/* Go ahead and parse it (todo: better heuristics for emitting FIN to the app level) */
std::string_view dataToConsume(data, length);
for (auto chunk : uWS::ChunkIterator(&dataToConsume, &remainingStreamingBytes)) {
dataHandler(user, chunk, chunk.length() == 0);
void *returnedUser = dataHandler(user, chunk, chunk.length() == 0);
if (returnedUser != user) {
/* The data handler closed or shut down the socket; stop parsing
* so we do not dispatch pipelined requests on a dead socket. */
return HttpParserResult::success(consumedTotal, returnedUser);
}
}
if (isParsingInvalidChunkedEncoding(remainingStreamingBytes)) [[unlikely]] {
// TODO: what happen if we already responded?
Expand All @@ -969,12 +974,16 @@ namespace uWS
} else if (contentLengthStringLen) {
if constexpr (!ConsumeMinimally) {
unsigned int emittable = (unsigned int) std::min<uint64_t>(remainingStreamingBytes, length);
dataHandler(user, std::string_view(data, emittable), emittable == remainingStreamingBytes);
void *returnedUser = dataHandler(user, std::string_view(data, emittable), emittable == remainingStreamingBytes);
remainingStreamingBytes -= emittable;

data += emittable;
length -= emittable;
consumedTotal += emittable;

if (returnedUser != user) {
return HttpParserResult::success(consumedTotal, returnedUser);
}
}
} else if(isConnectRequest) {
// This only serves to mark that the connect request read all headers
Expand All @@ -986,7 +995,10 @@ namespace uWS
break;
} else {
/* If we came here without a body; emit an empty data chunk to signal no data */
dataHandler(user, {}, true);
void *returnedUser = dataHandler(user, {}, true);
if (returnedUser != user) {
return HttpParserResult::success(consumedTotal, returnedUser);
}
}

/* Consume minimally should break as easrly as possible */
Expand All @@ -1011,7 +1023,10 @@ namespace uWS
/* It's either chunked or with a content-length */
std::string_view dataToConsume(data, length);
for (auto chunk : uWS::ChunkIterator(&dataToConsume, &remainingStreamingBytes)) {
dataHandler(user, chunk, chunk.length() == 0);
void *returnedUser = dataHandler(user, chunk, chunk.length() == 0);
if (returnedUser != user) {
return HttpParserResult::success(0, returnedUser);
}
}
if (isParsingInvalidChunkedEncoding(remainingStreamingBytes)) {
return HttpParserResult::error(HTTP_ERROR_400_BAD_REQUEST, HTTP_PARSER_ERROR_INVALID_CHUNKED_ENCODING);
Expand Down Expand Up @@ -1074,7 +1089,10 @@ namespace uWS
/* It's either chunked or with a content-length */
std::string_view dataToConsume(data, length);
for (auto chunk : uWS::ChunkIterator(&dataToConsume, &remainingStreamingBytes)) {
dataHandler(user, chunk, chunk.length() == 0);
void *returnedUser = dataHandler(user, chunk, chunk.length() == 0);
if (returnedUser != user) {
return HttpParserResult::success(0, returnedUser);
}
}
if (isParsingInvalidChunkedEncoding(remainingStreamingBytes)) {
return HttpParserResult::error(HTTP_ERROR_400_BAD_REQUEST, HTTP_PARSER_ERROR_INVALID_CHUNKED_ENCODING);
Expand Down
3 changes: 3 additions & 0 deletions packages/bun-uws/src/PerMessageDeflate.h
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,9 @@ struct InflationStream {

if (res == 0) {
/* Fast path wins */
if (written > maxPayloadLength) {
return std::nullopt;
}
return std::string_view(buf, written);
}
#endif
Expand Down
2 changes: 1 addition & 1 deletion src/base64/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -327,7 +327,7 @@ pub mod vlq {
let encoded_ = &encoded[start..][0..(encoded.len() - start).min(VLQ_MAX_IN_BYTES + 1)];

// inlining helps for the 1 or 2 byte case, hurts a little for larger
for i in 0..(VLQ_MAX_IN_BYTES + 1) {
for i in 0..encoded_.len() {
if ASSERT_VALID {
debug_assert!(encoded_[i] < U7_MAX); // invalid base64 character
}
Expand Down
4 changes: 3 additions & 1 deletion src/bun_core/string/HashedString.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,9 @@ impl HashedString {
}

pub fn eql_bytes(&self, other: &[u8]) -> bool {
(self.len as usize) == other.len() && (hash(other) as u32) == self.hash
(self.len as usize) == other.len()
&& (hash(other) as u32) == self.hash
&& self.str() == other
}

pub fn str(&self) -> &[u8] {
Expand Down
4 changes: 3 additions & 1 deletion src/glob/matcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -397,7 +397,9 @@ fn glob_match_impl(
let pi = state.path_index as usize;
let gi = state.glob_index as usize;
let n = cc_len as usize;
pi + n <= path.len() && path[pi..pi + n] == glob[gi..gi + n]
pi + n <= path.len()
&& gi + n <= glob.len()
&& path[pi..pi + n] == glob[gi..gi + n]
} else {
path[state.path_index as usize] == cc
};
Expand Down
17 changes: 13 additions & 4 deletions src/http/HTTPContext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -772,10 +772,13 @@ impl<const SSL: bool> HTTPContext<SSL> {
continue;
}

// The hash covers the Host-header SNI override that the handshake
// was verified against (see get_tls_hostname / connect()).
if socket.proxy_auth_hash != proxy_auth_hash {
continue;
}

if want_tunnel {
if socket.proxy_auth_hash != proxy_auth_hash {
continue;
}
if socket.target_port != target_port {
continue;
}
Expand Down Expand Up @@ -970,7 +973,13 @@ impl<const SSL: bool> HTTPContext<SSL> {
} else {
0
};
let proxy_auth_hash: u64 = if want_tunnel {
// For a direct TLS connection the handshake verifies the peer
// against get_tls_hostname() — which prefers the Host-header
// override (client.hostname) over url.hostname — so the override
// must discriminate the pool key there too, not just for CONNECT
// tunnels. proxy_auth_hash() reduces to exactly the override hash
// (or 0) for a non-proxied request.
let proxy_auth_hash: u64 = if want_tunnel || (SSL && client.http_proxy.is_none()) {
client.proxy_auth_hash()
} else {
0
Expand Down
7 changes: 7 additions & 0 deletions src/http/InternalState.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,12 @@ pub struct InternalStateFlags {
pub is_redirect_pending: bool,
pub is_libdeflate_fast_path_disabled: bool,
pub resend_request_body_on_redirect: bool,
/// Cross-origin redirect: the per-request Host override must be dropped so
/// the follow-up connection re-derives SNI/Host from the redirect target.
/// The actual clear is deferred to `do_redirect`, after the old socket's
/// pool/close decision — that decision needs `hostname` still set to know
/// the handshake was verified against an override.
pub clear_hostname_on_redirect: bool,
}

impl InternalStateFlags {
Expand All @@ -74,6 +80,7 @@ impl InternalStateFlags {
is_redirect_pending: false,
is_libdeflate_fast_path_disabled: false,
resend_request_body_on_redirect: false,
clear_hostname_on_redirect: false,
}
}
}
Expand Down
10 changes: 9 additions & 1 deletion src/http/h2_client/dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -619,7 +619,7 @@ pub fn decode_header_block(session: &mut ClientSession, stream: &mut Stream) {
if stream.status_code != 0 || malformed {
continue;
}
if is_malformed_response_field(result.name) {
if is_malformed_response_field(result.name) || is_malformed_response_value(result.value) {
malformed = true;
continue;
}
Expand Down Expand Up @@ -754,6 +754,14 @@ pub fn is_malformed_response_field(name: &[u8]) -> bool {
)
}

/// RFC 9113 §8.2.1: a field value MUST NOT contain NUL (0x00), LF (0x0a), or
/// CR (0x0d). HPACK is length-prefixed so these would otherwise pass through
/// verbatim, breaking the no-CR/LF invariant the HTTP/1.1 parser provides and
/// enabling header injection when values are forwarded downstream.
pub fn is_malformed_response_value(value: &[u8]) -> bool {
value.iter().any(|&c| c == 0 || c == b'\r' || c == b'\n')
}

pub fn error_code_for(err: bun_core::Error) -> wire::ErrorCode {
// PORT NOTE: bun_core::Error is a NonZeroU16 interned tag; `err!()` yields
// a const Error per name once the link-time table lands. Until then all
Expand Down
Loading
Loading