Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
6241286
node:path: bound UNC resolve buffer writes
Jarred-Sumner May 20, 2026
266e490
sql: tighten postgres type-name validation
Jarred-Sumner May 20, 2026
81171c1
sql/postgres: tighten SCRAM continue handling
Jarred-Sumner May 20, 2026
c04ef8e
http2: tighten inbound frame and header-block handling
Jarred-Sumner May 20, 2026
756112e
sql/postgres: tighten TLS requirement for verify modes
Jarred-Sumner May 20, 2026
9a1990b
fetch: bound response header buffering
Jarred-Sumner May 20, 2026
91fffa5
websocket: bound inbound message buffering
Jarred-Sumner May 20, 2026
0b24c26
webcore/blob: tighten UTF-16 slice and multipart field handling
Jarred-Sumner May 20, 2026
47b5d83
install: tighten lockfile git tag and integrity validation
Jarred-Sumner May 20, 2026
7c69a45
sql/mysql: gate public-key retrieval behind explicit option
Jarred-Sumner May 20, 2026
2d9f746
valkey: bound RESP blob length handling
Jarred-Sumner May 20, 2026
27ddcfc
archive: tighten symlink target normalization
Jarred-Sumner May 20, 2026
c81d54c
Bun.serve: tighten content-type header validation
Jarred-Sumner May 20, 2026
8c02431
resolver: tighten exports target segment validation
Jarred-Sumner May 20, 2026
a419de2
install/lockfile: bound string-buffer slice handling
Jarred-Sumner May 20, 2026
f29b35f
bake: tighten Host validation for dev-server internal routes
Jarred-Sumner May 20, 2026
e6b77c2
sql: tighten savepoint name validation
Jarred-Sumner May 20, 2026
29d3eb7
install/patch: tighten patch path validation
Jarred-Sumner May 20, 2026
2a0d871
node:child_process: tighten Windows batch-file spawn handling
Jarred-Sumner May 20, 2026
337ca9c
node:net: tighten BlockList structured-clone validation
Jarred-Sumner May 20, 2026
81ff83b
http: tighten header field-name validation
Jarred-Sumner May 20, 2026
c09abc0
install: tighten bin name normalization
Jarred-Sumner May 20, 2026
f3580e6
sys: tighten temp-file open flags
Jarred-Sumner May 20, 2026
db9b478
s3: bound upload-id parse handling
Jarred-Sumner May 20, 2026
5e68d84
node:http: tighten connections-list iteration
Jarred-Sumner May 20, 2026
1f8c074
router: bound URL path index handling
Jarred-Sumner May 20, 2026
7ec002c
websocket: tighten Sec-WebSocket-Version comparison
Jarred-Sumner May 20, 2026
158bd91
css: bound nested-block recursion
Jarred-Sumner May 20, 2026
d235788
webcore/serialization: bound key buffer indexing
Jarred-Sumner May 20, 2026
d44f742
sqlite: tighten this-value validation
Jarred-Sumner May 20, 2026
86405fe
node:buffer: tighten write target validation
Jarred-Sumner May 20, 2026
ec60305
webcrypto: bound OKP key-data parsing
Jarred-Sumner May 20, 2026
dec2777
glob: bound brace-branch matching
Jarred-Sumner May 20, 2026
59b5f98
debugger: tighten inspector path id generation
Jarred-Sumner May 20, 2026
2e5a926
bundler: tighten [dir] placeholder handling
Jarred-Sumner May 20, 2026
c32c9fc
websocket: tighten Sec-WebSocket-Key handling
Jarred-Sumner May 20, 2026
25bdc86
node:http: tighten response header name handling
Jarred-Sumner May 20, 2026
c19b31b
[autofix.ci] apply automated fixes
autofix-ci[bot] May 20, 2026
990049e
install: drop --end-of-options from git checkout argv (subcommand doe…
Jarred-Sumner May 20, 2026
13ac940
address review: tighten install resolved-tag validation
Jarred-Sumner May 20, 2026
f0fd736
address review: tighten sql savepoint argument handling
Jarred-Sumner May 20, 2026
f07b1d1
address review: tighten mysql connection option handling
Jarred-Sumner May 20, 2026
af2f7b9
address review: tighten dev server websocket upgrade handling
Jarred-Sumner May 20, 2026
78e5725
address review: tighten path resolve buffer handling
Jarred-Sumner May 20, 2026
c0c6210
address review: tighten semver string handle comparison
Jarred-Sumner May 20, 2026
800558f
address review: tighten mysql auth test coverage
Jarred-Sumner May 20, 2026
28b2c0b
address review: tighten http2 continuation frame size handling
Jarred-Sumner May 20, 2026
f9f2675
address review: tighten http2 continuation end-stream handling
Jarred-Sumner May 20, 2026
c87c50c
address review: tighten http2 inbound stream lookup handling
Jarred-Sumner May 20, 2026
1e23f29
install: emit warning instead of hard error for unrecognized integrit…
Jarred-Sumner May 20, 2026
175e9b6
fetch: bound response header buffer with a fixed cap independent of r…
Jarred-Sumner May 20, 2026
33af6a5
node:child_process: align batch-file rejection with Node error shape
Jarred-Sumner May 20, 2026
d9a8251
address review: clear unsupported lockfile integrity after warning
Jarred-Sumner May 20, 2026
572db50
address review: align spawnSync batch-file error message; drop tautol…
Jarred-Sumner May 20, 2026
ef8a25b
address review: bound remaining OCTET STRING length-byte reads in OKP…
Jarred-Sumner May 20, 2026
c5a7d6c
address review: bound BIT STRING length-byte read in importSpki
Jarred-Sumner May 20, 2026
fa22e3d
node:buffer: short-circuit zero-remaining write after re-validation
Jarred-Sumner May 20, 2026
7f81d7e
node:net: bind structured-clone payload to instance identity
Jarred-Sumner May 20, 2026
a9019b2
webcore/blob: note Zig-parity intent on UTF-16LE text branch
Jarred-Sumner May 20, 2026
1cb0522
bake: drop dead inherent on_web_socket_upgrade method
Jarred-Sumner May 20, 2026
941e4b4
address review: allow parens/comma/dot in array type-name allowlist
Jarred-Sumner May 20, 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
11 changes: 11 additions & 0 deletions packages/bun-types/sql.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,17 @@ declare module "bun" {
* @default true
*/
prepare?: boolean | undefined;

/**
* MySQL only. Allow the client to request the server's RSA public key
* during `caching_sha2_password` / `sha256_password` authentication when
* the connection is not protected by TLS. Disabled by default because a
* network attacker can substitute their own key and recover the
* plaintext password. Enable only for trusted local connections, or use
* TLS instead.
* @default false
*/
allowPublicKeyRetrieval?: boolean | undefined;
}

/**
Expand Down
6 changes: 6 additions & 0 deletions packages/bun-uws/src/HttpParser.h
Original file line number Diff line number Diff line change
Expand Up @@ -750,6 +750,12 @@ namespace uWS
/* Error: invalid chars in field name */
return HttpParserResult::error(HTTP_ERROR_400_BAD_REQUEST, HTTP_PARSER_ERROR_INVALID_HEADER_TOKEN);
}
/* RFC 9112 5.1: field-name is a non-empty token. An empty name would also
* collide with the end-of-headers sentinel and hide later headers from the
* Content-Length / Transfer-Encoding request-smuggling checks. */
if (headers->key.length() == 0) {
return HttpParserResult::error(HTTP_ERROR_400_BAD_REQUEST, HTTP_PARSER_ERROR_INVALID_HEADER_TOKEN);
}
postPaddedBuffer++;

preliminaryValue = postPaddedBuffer;
Expand Down
4 changes: 3 additions & 1 deletion packages/bun-uws/src/HttpResponse.h
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,9 @@ struct HttpResponse : public AsyncSocket<SSL> {

/* Note: OpenSSL can be used here to speed this up somewhat */
char secWebSocketAccept[29] = {};
WebSocketHandshake::generate(secWebSocketKey.data(), secWebSocketAccept);
char secWebSocketKeyBuffer[24] = {};
secWebSocketKey.copy(secWebSocketKeyBuffer, 24);
WebSocketHandshake::generate(secWebSocketKeyBuffer, secWebSocketAccept);

writeStatus("101 Switching Protocols")
->writeHeader("Upgrade", "websocket")
Expand Down
21 changes: 17 additions & 4 deletions src/bundler/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2596,10 +2596,23 @@ pub(crate) fn path_template_print<W: bun_io::Write>(
};

match field {
PlaceholderField::Dir => PathTemplate::write_replacing_slashes_on_windows(
writer,
if !dir.is_empty() { dir } else { b"." },
)?,
PlaceholderField::Dir => {
if dir.is_empty() {
writer.write_all(b".")?;
} else {
// Sanitize leading `..` segments so `[dir]` cannot place output
// above outdir when a source resolves outside `root`.
let mut d: &[u8] = dir;
while matches!(d, [b'.', b'.', b'/' | b'\\', ..]) {
PathTemplate::write_replacing_slashes_on_windows(writer, b"_.._/")?;
d = &d[3..];
}
PathTemplate::write_replacing_slashes_on_windows(
writer,
if d == b".." { b"_.._" } else { d },
)?;
}
}
PlaceholderField::Name => {
PathTemplate::write_replacing_slashes_on_windows(writer, name)?
}
Expand Down
13 changes: 13 additions & 0 deletions src/css/css_parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -634,6 +634,8 @@ pub fn parse_until_after<T, C>(
result
}

const MAX_NESTING_DEPTH: u32 = 512;

fn parse_nested_block<T>(
parser: &mut Parser,
parsefn: impl FnOnce(&mut Parser) -> CssResult<T>,
Expand All @@ -646,6 +648,14 @@ fn parse_nested_block<T>(
)
});

parser.input.nesting_depth += 1;
if parser.input.nesting_depth > MAX_NESTING_DEPTH {
parser.input.nesting_depth -= 1;
let err = parser.new_custom_error(ParserError::maximum_nesting_depth);
consume_until_end_of_block(block_type, &mut parser.input.tokenizer);
return Err(err);
}

let closing_delimiter = match block_type {
BlockType::CurlyBracket => Delimiters::CLOSE_CURLY_BRACKET,
BlockType::SquareBracket => Delimiters::CLOSE_SQUARE_BRACKET,
Expand All @@ -663,6 +673,7 @@ fn parse_nested_block<T>(
}
parser.stop_before = saved_stop_before;
consume_until_end_of_block(block_type, &mut parser.input.tokenizer);
parser.input.nesting_depth -= 1;
result
}

Expand Down Expand Up @@ -4201,6 +4212,7 @@ impl Delimiters {
pub struct ParserInput<'a> {
pub tokenizer: Tokenizer<'a>,
pub cached_token: Option<CachedToken>,
pub nesting_depth: u32,
}

impl<'a> ParserInput<'a> {
Expand All @@ -4216,6 +4228,7 @@ impl<'a> ParserInput<'a> {
ParserInput {
tokenizer: Tokenizer::init_with_arena(code, arena),
cached_token: None,
nesting_depth: 0,
}
}
}
Expand Down
44 changes: 40 additions & 4 deletions src/glob/matcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ struct Brace {
}
type BraceStack = BoundedArray<Brace, 10>;

/// Upper bound on brace-branch alternatives explored per `match` call. Sequential
/// brace groups multiply (`{a,b}{c,d}` = 4 alternatives), so without a cap an
/// adversarial pattern of ten sequential 10-way groups would explore 10^10
/// alternatives. Patterns that exceed this budget fail to match.
const BRACE_BRANCH_BUDGET: u32 = 10_000;

// PORT NOTE: made `pub` — Zig leaks this private type through `pub fn match`; Rust forbids private-in-public.
#[derive(Copy, Clone, Eq, PartialEq)]
pub enum MatchResult {
Expand Down Expand Up @@ -141,7 +147,15 @@ pub fn r#match(glob: &[u8], path: &[u8]) -> MatchResult {

// PORT NOTE: `BraceStack.init(0) catch unreachable` — zero-length init cannot fail.
let mut brace_stack = BraceStack::default();
let matched = glob_match_impl(&mut state, glob, 0, path, &mut brace_stack);
let mut brace_budget = BRACE_BRANCH_BUDGET;
let matched = glob_match_impl(
&mut state,
glob,
0,
path,
&mut brace_stack,
&mut brace_budget,
);

// TODO: consider just returning a bool
// return matched != negated;
Expand Down Expand Up @@ -170,6 +184,7 @@ fn glob_match_impl(
glob_start: u32,
path: &[u8],
brace_stack: &mut BraceStack,
brace_budget: &mut u32,
) -> bool {
'main_loop: while (state.glob_index as usize) < glob.len()
|| (state.path_index as usize) < path.len()
Expand Down Expand Up @@ -348,7 +363,7 @@ fn glob_match_impl(
continue 'main_loop;
}
}
return match_brace(state, glob, path, brace_stack);
return match_brace(state, glob, path, brace_stack, brace_budget);
}
b',' => {
if state.brace_depth > 0 {
Expand Down Expand Up @@ -412,7 +427,13 @@ fn glob_match_impl(
true
}

fn match_brace(state: &mut State, glob: &[u8], path: &[u8], brace_stack: &mut BraceStack) -> bool {
fn match_brace(
state: &mut State,
glob: &[u8],
path: &[u8],
brace_stack: &mut BraceStack,
brace_budget: &mut u32,
) -> bool {
let mut brace_depth: i16 = 0;
let mut in_brackets = false;

Expand Down Expand Up @@ -441,6 +462,7 @@ fn match_brace(state: &mut State, glob: &[u8], path: &[u8], brace_stack: &mut Br
open_brace_index,
branch_index,
brace_stack,
brace_budget,
) {
return true;
}
Expand All @@ -457,6 +479,7 @@ fn match_brace(state: &mut State, glob: &[u8], path: &[u8], brace_stack: &mut Br
open_brace_index,
branch_index,
brace_stack,
brace_budget,
) {
return true;
}
Expand Down Expand Up @@ -485,7 +508,13 @@ fn match_brace_branch(
open_brace_index: u32,
branch_index: u32,
brace_stack: &mut BraceStack,
brace_budget: &mut u32,
) -> bool {
if *brace_budget == 0 {
return false;
}
*brace_budget -= 1;

// exceeded brace depth
let Ok(()) = brace_stack.push(Brace {
open_brace_idx: open_brace_index,
Expand All @@ -499,7 +528,14 @@ fn match_brace_branch(
branch_state.glob_index = branch_index;
branch_state.brace_depth = u8::try_from(brace_stack.len()).expect("int cast");

let matched = glob_match_impl(&mut branch_state, glob, branch_index, path, brace_stack);
let matched = glob_match_impl(
&mut branch_state,
glob,
branch_index,
path,
brace_stack,
brace_budget,
);

let _ = brace_stack.pop();

Expand Down
11 changes: 11 additions & 0 deletions src/http/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3104,6 +3104,17 @@ impl<'a> HTTPClient<'a> {
) {
Ok(r) => r,
Err(picohttp::ParseResponseError::ShortRead) => {
// `MAX_HTTP_HEADER_SIZE` (default 16 KB) is the *server*/
// request-side knob (Node `--max-http-header-size`); reusing
// it here rejects legitimate responses with large
// `Location`/`Set-Cookie` headers. The intent is to bound
// `response_message_buffer` growth, so use a generous fixed
// cap independent of that knob.
const MAX_RESPONSE_HEADER_BUFFER: usize = 1024 * 1024;
if to_read!().len() > MAX_RESPONSE_HEADER_BUFFER {
self.close_and_fail::<IS_SSL>(err!(ResponseHeadersTooLarge), socket);
return;
}
self.handle_short_read::<IS_SSL>(incoming_data, socket, needs_move);
return;
}
Expand Down
15 changes: 15 additions & 0 deletions src/http_jsc/websocket_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ pub type Socket<const SSL: bool> = NewSocketHandler<SSL>;
const STACK_FRAME_SIZE: usize = 1024;
/// Minimum message size to compress (RFC 7692 recommendation)
const MIN_COMPRESS_SIZE: usize = 860;
/// Maximum buffered inbound message size (128 MB). A server that declares a
/// larger frame, or whose continuation fragments accumulate past this, fails
/// the connection with close code 1009 instead of growing `receive_buffer`
/// without bound.
const MAX_RECEIVE_MESSAGE_LENGTH: usize = 128 * 1024 * 1024;
#[derive(bun_ptr::CellRefCounted)]
#[ref_count(destroy = Self::deinit)]
pub struct WebSocket<const SSL: bool> {
Expand Down Expand Up @@ -946,6 +951,16 @@ impl<const SSL: bool> WebSocket<SSL> {
}
}
ReceiveState::NeedBody => {
if self
.receive_buffer
.readable_length()
.saturating_add(receive_body_remain)
> MAX_RECEIVE_MESSAGE_LENGTH
{
self.terminate(ErrorCode::MessageTooBig);
terminated = true;
break;
}
let to_consume = receive_body_remain.min(data.len());

let consumed = self.consume(
Expand Down
2 changes: 1 addition & 1 deletion src/http_jsc/websocket_client/WebSocketUpgradeClient.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1332,7 +1332,7 @@ impl<const SSL: bool> HTTPClient<SSL> {
header.name(),
b"Sec-WebSocket-Version",
) {
if !strings::eql_comptime_ignore_len(header.value(), b"13") {
if !strings::eql_comptime(header.value(), b"13") {
// SAFETY: no `&mut Self` is live across this call.
unsafe { Self::terminate(this, ErrorCode::InvalidWebsocketVersion) };
return;
Expand Down
10 changes: 5 additions & 5 deletions src/http_types/URLPath.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,13 +98,13 @@ pub fn parse(possibly_encoded_pathname_: &[u8]) -> Result<URLPath, bun_core::Err
decoded_pathname = decoded_storage.as_deref().unwrap();
}

let mut question_mark_i: i16 = -1;
let mut period_i: i16 = -1;
let mut question_mark_i: i32 = -1;
let mut period_i: i32 = -1;

let mut first_segment_end: i16 = i16::MAX;
let mut last_slash: i16 = -1;
let mut first_segment_end: i32 = i32::MAX;
let mut last_slash: i32 = -1;

let mut i: i16 = i16::try_from(decoded_pathname.len()).expect("int cast") - 1;
let mut i: i32 = i32::try_from(decoded_pathname.len()).expect("int cast") - 1;

while i >= 0 {
let c = decoded_pathname[usize::try_from(i).expect("int cast")];
Expand Down
11 changes: 9 additions & 2 deletions src/install/bin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -740,11 +740,18 @@ pub type Context = PriorityQueueContext;

// https://github.com/npm/npm-normalize-package-bin/blob/574e6d7cd21b2f3dee28a216ec2053c2551f7af9/lib/index.js#L38
pub fn normalized_bin_name(name: &[u8]) -> &[u8] {
if let Some(i) = name
let name = match name
.iter()
.rposition(|&b| b == b'/' || b == b'\\' || b == b':')
{
return &name[i + 1..];
Some(i) => &name[i + 1..],
None => name,
};

// npm's `join('/', key).slice(1)` collapses `.`/`..` to empty; do the same
// so the `.bin/<name>` destination cannot resolve outside `.bin/`.
if name == b"." || name == b".." {
return b"";
}

name
Expand Down
34 changes: 34 additions & 0 deletions src/install/lockfile/bun.lock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2483,13 +2483,34 @@ pub fn parse_into_binary_lockfile(
};

pkg.meta.integrity = Integrity::parse(integrity_str);
if !integrity_str.is_empty() && !pkg.meta.integrity.tag.is_supported() {
// Surface — don't fail — for npm parity (`npm install`
// proceeds on a malformed lockfile integrity, treating
// it as absent). The download path still applies any
// registry-supplied integrity, so this only loses the
// *lockfile* pin.
log.add_warning(
Some(source),
integrity_expr.loc,
b"Unsupported or malformed integrity hash; ignoring",
);
Comment thread
Jarred-Sumner marked this conversation as resolved.
pkg.meta.integrity = Integrity::default();
}
}
ResolutionTag::LocalTarball | ResolutionTag::RemoteTarball => {
// integrity is optional for tarball deps (backward compat)
if i < (pkg_info.len_u32() as usize) {
let integrity_expr = pkg_info.at(i);
if let Some(integrity_str) = integrity_expr.as_utf8_string_literal() {
pkg.meta.integrity = Integrity::parse(integrity_str);
if !integrity_str.is_empty() && !pkg.meta.integrity.tag.is_supported() {
log.add_warning(
Some(source),
integrity_expr.loc,
b"Unsupported or malformed integrity hash; ignoring",
);
pkg.meta.integrity = Integrity::default();
}
}
}
}
Expand All @@ -2508,6 +2529,11 @@ pub fn parse_into_binary_lockfile(
return Err(ParseError::InvalidPackageInfo);
};

if !crate::repository::is_safe_resolved_tag(bun_tag_str) {
log.add_error(Some(source), bun_tag.loc, b"Invalid git dependency tag");
return Err(ParseError::InvalidPackageInfo);
}

let resolved = sbuf!(lockfile).append(bun_tag_str)?;
if tag == ResolutionTag::Git {
res.git_mut().resolved = resolved;
Expand All @@ -2520,6 +2546,14 @@ pub fn parse_into_binary_lockfile(
let integrity_expr = pkg_info.at(i);
if let Some(integrity_str) = integrity_expr.as_utf8_string_literal() {
pkg.meta.integrity = Integrity::parse(integrity_str);
if !integrity_str.is_empty() && !pkg.meta.integrity.tag.is_supported() {
log.add_warning(
Some(source),
integrity_expr.loc,
b"Unsupported or malformed integrity hash; ignoring",
);
pkg.meta.integrity = Integrity::default();
}
}
}
}
Expand Down
Loading
Loading