Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
73 commits
Select commit Hold shift + click to select a range
ad9aa4e
install: tighten package folder name validation
Jarred-Sumner May 26, 2026
6947c6a
install: tighten lockfile migration resolution validation
Jarred-Sumner May 26, 2026
71370b2
install: tighten folder dependency path validation
Jarred-Sumner May 26, 2026
8ee9f79
install: tighten lockfile string serialization
Jarred-Sumner May 26, 2026
6100d16
install: tighten manifest string handling
Jarred-Sumner May 26, 2026
9a2b0cd
run: tighten script interpreter resolution
Jarred-Sumner May 26, 2026
d2bb24b
install: bound cache folder name formatting
Jarred-Sumner May 26, 2026
89ac8b1
install: tighten trusted dependency matching
Jarred-Sumner May 26, 2026
2b7e860
install: tighten bin link target validation
Jarred-Sumner May 26, 2026
a901b4e
bunx: tighten cache directory ownership checks
Jarred-Sumner May 26, 2026
1783f06
install: tighten migrated git reference validation
Jarred-Sumner May 26, 2026
78b727f
install: tighten yarn lockfile output quoting
Jarred-Sumner May 26, 2026
1ca1d9c
install: bound tarball decompression output
Jarred-Sumner May 26, 2026
77a37a7
test: add regression coverage for input validation changes
Jarred-Sumner May 26, 2026
82c2b1b
fetch: tighten body content type handling
Jarred-Sumner May 26, 2026
d7beff4
node:fs: tighten async write buffer handling
Jarred-Sumner May 26, 2026
e800a24
webcrypto: tighten key serialization validation
Jarred-Sumner May 26, 2026
5daaf74
s3: tighten content type validation
Jarred-Sumner May 26, 2026
e03ceae
FormData: tighten indexed key handling
Jarred-Sumner May 26, 2026
990dcbf
sql: bound column name lookup
Jarred-Sumner May 26, 2026
2c447b2
node:crypto: tighten ecdh secret handling
Jarred-Sumner May 26, 2026
feadb6e
blob: tighten utf-16 text decoding
Jarred-Sumner May 26, 2026
f667dc3
node:crypto: tighten rsa decrypt error handling
Jarred-Sumner May 26, 2026
4d48c32
blob: bound deserialization length handling
Jarred-Sumner May 26, 2026
716cfda
blob: tighten serialization of partial views
Jarred-Sumner May 26, 2026
e4fc15b
node:fs: bound recursive directory path handling
Jarred-Sumner May 26, 2026
93959fe
test: add regression coverage for input validation changes
Jarred-Sumner May 26, 2026
b0ccb2d
server: tighten response handler state handling
Jarred-Sumner May 26, 2026
d434a77
fetch: tighten request header handling
Jarred-Sumner May 26, 2026
3d86750
tls: tighten handshake state handling
Jarred-Sumner May 26, 2026
17ee952
node:http2: bound frame payload handling
Jarred-Sumner May 26, 2026
c2f48ed
node:http: tighten request option validation
Jarred-Sumner May 26, 2026
97e95a5
node:dns: tighten hostname validation
Jarred-Sumner May 26, 2026
572a530
bun-vscode: tighten diagnostics socket setup
Jarred-Sumner May 26, 2026
2f3b661
fetch: tighten protocol selection handling
Jarred-Sumner May 26, 2026
5aa47d7
bake: bound path handling
Jarred-Sumner May 26, 2026
c1f2f7b
valkey: bound protocol scan handling
Jarred-Sumner May 26, 2026
0ed4038
server: tighten debug route handling
Jarred-Sumner May 26, 2026
355d330
test: add regression coverage for input validation changes
Jarred-Sumner May 26, 2026
02a61cb
escapeHTML: tighten codepoint validation
Jarred-Sumner May 26, 2026
c54abbc
markdown: tighten link metadata validation
Jarred-Sumner May 26, 2026
acaa519
markdown: bound delimiter handling
Jarred-Sumner May 26, 2026
45fdb90
json5: bound nested value handling
Jarred-Sumner May 26, 2026
af25de9
shell: tighten redirect target validation
Jarred-Sumner May 26, 2026
5883e37
shell: tighten redirect buffer handling
Jarred-Sumner May 26, 2026
3d2ecc8
resolver: tighten exports target validation
Jarred-Sumner May 26, 2026
f9cc26c
yaml: bound merge key handling
Jarred-Sumner May 26, 2026
87ead14
shell: tighten cd argument validation
Jarred-Sumner May 26, 2026
ff4093a
css: tighten tokenizer bounds validation
Jarred-Sumner May 26, 2026
48d72e6
glob: tighten entry offset validation
Jarred-Sumner May 26, 2026
bad20fb
test: add regression coverage for input validation changes
Jarred-Sumner May 26, 2026
56972d1
test: add dns hostname validation coverage
Jarred-Sumner May 26, 2026
8712266
[autofix.ci] apply automated fixes
autofix-ci[bot] May 26, 2026
cd0592b
blob: keep zero-copy utf-16 decoding for aligned views
Jarred-Sumner May 26, 2026
b5dce42
install: keep local tarball paths exempt from package name validation
Jarred-Sumner May 26, 2026
cb1467c
install: reject overlong folder dependency paths and tighten install …
Jarred-Sumner May 26, 2026
ac98c11
md: keep prefetched image keys consistent with the render path
Jarred-Sumner May 26, 2026
438ea30
server: keep /bun:info available on unix socket listeners in development
Jarred-Sumner May 26, 2026
5e45673
test: tighten json5 depth probe and fetch test assertions
Jarred-Sumner May 26, 2026
e055bf5
test: move valkey incremental reply scanning into its own file
Jarred-Sumner May 26, 2026
76b7ed5
blob: use fixed-size chunks in the utf-16 fallback decode
Jarred-Sumner May 26, 2026
e48e1e7
test: use tempDir helper for unix socket path in serve test
Jarred-Sumner May 26, 2026
bfd2adf
install: limit bin target resolution to suspicious paths
Jarred-Sumner May 26, 2026
f36830a
install: raise tarball decompression limit
Jarred-Sumner May 26, 2026
5fd031a
test: add migration coverage for remote tarball dependencies
Jarred-Sumner May 26, 2026
d81cd2e
node:fs: move async buffer pinning into the conversion layer
Jarred-Sumner May 26, 2026
2d20c50
server: move the loopback check onto the socket address
Jarred-Sumner May 26, 2026
d3bcb9e
shell: use an owned copy for stdin buffer redirects
Jarred-Sumner May 26, 2026
bd8d651
fetch: avoid cloning interned content types
Jarred-Sumner May 26, 2026
5871e6d
install: scope the bin target containment probe to the parent directory
Jarred-Sumner May 26, 2026
999d103
Revert "bunx: tighten cache directory ownership checks"
Jarred-Sumner May 26, 2026
05f0e92
test: tolerate restricted symlink creation in glob scan test
Jarred-Sumner May 26, 2026
bb9c7cf
install: report the package name when streaming extraction rejects it
Jarred-Sumner May 26, 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
3 changes: 2 additions & 1 deletion packages/bun-debug-adapter-protocol/src/debugger/signal.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { randomBytes } from "node:crypto";
import { EventEmitter } from "node:events";
import type { Server, Socket } from "node:net";
import { createServer } from "node:net";
Expand Down Expand Up @@ -77,7 +78,7 @@ export class UnixSignal extends EventEmitter<UnixSignalEventMap> {
}

export function randomUnixPath(): string {
return join(tmpdir(), `${Math.random().toString(36).slice(2)}.sock`);
return join(tmpdir(), `${randomBytes(16).toString("hex")}.sock`);
}

function parseUnixPath(path: string | URL): string {
Expand Down
1 change: 1 addition & 0 deletions packages/bun-usockets/src/crypto/openssl.c
Original file line number Diff line number Diff line change
Expand Up @@ -974,6 +974,7 @@ struct us_socket_t *us_internal_ssl_on_open(struct us_socket_t *s, int is_client
struct us_socket_t *result = us_dispatch_open(s, is_client, ip, ip_length);
if (!result || ssl_gone(result)) return result;
/* Kick the handshake immediately — some peers stall waiting for ClientHello. */
ssl_set_loop_data(result);
ssl_update_handshake(result);
return result;
}
Expand Down
56 changes: 18 additions & 38 deletions packages/bun-vscode/src/features/diagnostics/diagnostics.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import * as fs from "node:fs/promises";
import { Socket } from "node:net";
import * as os from "node:os";
import { inspect } from "node:util";
Expand All @@ -11,7 +10,6 @@ import {
} from "../../../../bun-debug-adapter-protocol";
import type { JSC } from "../../../../bun-inspector-protocol";
import { getConfig } from "../../extension";
import { typedGlobalState } from "../../global-state";

const output = vscode.window.createOutputChannel("Bun - Diagnostics");

Expand Down Expand Up @@ -76,48 +74,16 @@ class BunDiagnosticsManager {
return this.signal.url;
}

private static async getOrRecreateSignal(context: vscode.ExtensionContext) {
const globalState = typedGlobalState(context.globalState);
const existing = globalState.get("BUN_INSPECT_CONNECT_TO");

const isWin = os.platform() === "win32";

if (existing) {
if (existing.type === "unix") {
output.appendLine(`Reusing existing unix socket: ${existing.url}`);

if ("url" in existing) {
await fs.unlink(existing.url).catch(() => {
// ? lol
});
}

return new UnixSignal(existing.url);
} else {
output.appendLine(`Reusing existing tcp socket on: ${existing.port}`);
return new TCPSocketSignal(existing.port);
}
}

if (isWin) {
private static async createSignal(): Promise<UnixSignal | TCPSocketSignal> {
if (os.platform() === "win32") {
const port = await getAvailablePort();

await globalState.update("BUN_INSPECT_CONNECT_TO", {
type: "tcp",
port,
});

output.appendLine(`Created new tcp socket on: ${port}`);

return new TCPSocketSignal(port);
} else {
const signal = new UnixSignal();

await globalState.update("BUN_INSPECT_CONNECT_TO", {
type: "unix",
url: signal.url,
});

output.appendLine(`Created new unix socket: ${signal.url}`);

return signal;
Expand All @@ -137,7 +103,15 @@ class BunDiagnosticsManager {
// );

public static async initialize(context: vscode.ExtensionContext) {
const signal = await BunDiagnosticsManager.getOrRecreateSignal(context);
const signal = await BunDiagnosticsManager.createSignal();

try {
await signal.ready;
} catch (error) {
signal.close();
signal.removeAllListeners();
throw error;
}

return new BunDiagnosticsManager(context, signal);
}
Expand Down Expand Up @@ -271,7 +245,13 @@ export async function registerDiagnosticsSocket(context: vscode.ExtensionContext

if (!getConfig("diagnosticsSocket.enabled")) return;

const manager = await BunDiagnosticsManager.initialize(context);
let manager: BunDiagnosticsManager;
try {
manager = await BunDiagnosticsManager.initialize(context);
} catch (error) {
output.appendLine(`Failed to start diagnostics socket: ${error}`);
return;
}

context.environmentVariableCollection.replace("BUN_INSPECT_CONNECT_TO", manager.signalUrl);

Expand Down
15 changes: 9 additions & 6 deletions src/bun_core/string/immutable/escapeHTML.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use bun_alloc::AllocError;

use crate::string::strings::{
self, ASCII_U16_VECTOR_SIZE, ASCII_VECTOR_SIZE, AsciiU16Vector, AsciiVector, utf16_codepoint,
self, ASCII_U16_VECTOR_SIZE, ASCII_VECTOR_SIZE, AsciiU16Vector, AsciiVector,
utf16_codepoint_with_fffd,
};
use crate::string::w;

Expand Down Expand Up @@ -583,7 +584,8 @@ pub fn escape_html_for_utf16_input(utf16: &[u16]) -> Result<Escaped<u16>, AllocE
break 'lazy;
}
128..=u16::MAX => {
let cp = utf16_codepoint(&remaining[i as usize..]);
let cp =
utf16_codepoint_with_fffd(&remaining[i as usize..]);
i += u16::from(cp.len);
}
_ => {
Expand Down Expand Up @@ -621,7 +623,7 @@ pub fn escape_html_for_utf16_input(utf16: &[u16]) -> Result<Escaped<u16>, AllocE
i += 1;
}
128..=u16::MAX => {
let cp = utf16_codepoint(&remaining[i as usize..]);
let cp = utf16_codepoint_with_fffd(&remaining[i as usize..]);

buf.extend_from_slice(
&remaining[i as usize..][..usize::from(cp.len)],
Expand Down Expand Up @@ -683,7 +685,8 @@ pub fn escape_html_for_utf16_input(utf16: &[u16]) -> Result<Escaped<u16>, AllocE
i += 1;
}
128..=u16::MAX => {
let cp = utf16_codepoint(&remaining[i as usize..]);
let cp =
utf16_codepoint_with_fffd(&remaining[i as usize..]);

buf.extend_from_slice(
&remaining[i as usize..][..usize::from(cp.len)],
Expand Down Expand Up @@ -730,7 +733,7 @@ pub fn escape_html_for_utf16_input(utf16: &[u16]) -> Result<Escaped<u16>, AllocE
}
128..=u16::MAX => {
let avail = if idx + 1 == end { 1 } else { 2 };
let cp = utf16_codepoint(&remaining[idx..idx + avail]);
let cp = utf16_codepoint_with_fffd(&remaining[idx..idx + avail]);

idx += usize::from(cp.len);
}
Expand Down Expand Up @@ -766,7 +769,7 @@ pub fn escape_html_for_utf16_input(utf16: &[u16]) -> Result<Escaped<u16>, AllocE
}
128..=u16::MAX => {
let avail = if idx + 1 == end { 1 } else { 2 };
let cp = utf16_codepoint(&remaining[idx..idx + avail]);
let cp = utf16_codepoint_with_fffd(&remaining[idx..idx + avail]);

buf.extend_from_slice(&remaining[idx..idx + usize::from(cp.len)]);
idx += usize::from(cp.len);
Expand Down
1 change: 1 addition & 0 deletions src/cares_sys/c_ares.rs
Original file line number Diff line number Diff line change
Expand Up @@ -883,6 +883,7 @@ impl Channel {

pub fn resolve<T: ResolveHandler>(&mut self, name: &[u8], ctx: &mut T) {
if name.len() >= 1023
|| name.contains(&0)
|| (name.is_empty() && !(T::LOOKUP_NAME == b"ns" || T::LOOKUP_NAME == b"soa"))
{
// SAFETY: thunk handles ARES_EBADNAME path.
Expand Down
2 changes: 1 addition & 1 deletion src/css/css_parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5446,7 +5446,7 @@ impl<'a> Tokenizer<'a> {

pub fn consume_char(&mut self) -> u32 {
let c = self.next_char();
let len_utf8 = len_utf8(c);
let len_utf8 = len_utf8(c).min(self.src.len() - self.position);
self.position += len_utf8;
// Note that due to the special case for the 4-byte sequence intro,
// we must use wrapping add here.
Expand Down
22 changes: 9 additions & 13 deletions src/glob/GlobWalker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1083,13 +1083,11 @@ impl<'a, A: Accessor, const SENTINEL: bool> Iterator<'a, A, SENTINEL> {
}

let subdir_parts: &[&[u8]] = &[dir_dir_path, entry_name];
let entry_start: u32 = u32::try_from(if dir_dir_path.is_empty() {
0
} else {
dir_dir_path.len() + 1
})
.unwrap();
let subdir_entry_name = self.walker.join(subdir_parts)?;
let joined = work_item_logical_path(&subdir_entry_name);
let entry_start: u32 =
u32::try_from(joined.len() - strings::basename(joined).len())
.unwrap();

self.walker.workbuf.push(WorkItem::new_symlink(
subdir_entry_name,
Expand Down Expand Up @@ -1170,14 +1168,12 @@ impl<'a, A: Accessor, const SENTINEL: bool> Iterator<'a, A, SENTINEL> {
bun_sys::FileKind::SymLink => {
if self.walker.follow_symlinks {
let subdir_parts: &[&[u8]] = &[dir_dir_path, entry_name];
let entry_start: u32 =
u32::try_from(if dir_dir_path.is_empty() {
0
} else {
dir_dir_path.len() + 1
})
.unwrap();
let subdir_entry_name = self.walker.join(subdir_parts)?;
let joined = work_item_logical_path(&subdir_entry_name);
let entry_start: u32 = u32::try_from(
joined.len() - strings::basename(joined).len(),
)
.unwrap();
self.walker.workbuf.push(WorkItem::new_symlink(
subdir_entry_name,
active,
Expand Down
19 changes: 19 additions & 0 deletions src/http/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1717,9 +1717,20 @@ impl<'a> HTTPClient<'a> {
) {
return false;
}
if self.has_tls_options_unsupported_by_h3() {
return false;
}
h3_alt_svc_enabled()
}

fn has_tls_options_unsupported_by_h3(&self) -> bool {
self.signals.get(signals::Field::CertErrors)
|| self
.tls_props
.as_ref()
.is_some_and(|tls| tls.get().requires_custom_request_ctx)
}

pub fn first_call<const IS_SSL: bool>(&mut self, socket: HttpSocket<IS_SSL>) {
if FeatureFlags::IS_FETCH_PRECONNECT_SUPPORTED {
if self.flags.is_preconnect_only {
Expand Down Expand Up @@ -2165,6 +2176,9 @@ impl<'a> HTTPClient<'a> {
}
}
h if h == hash_header_const(CHUNKED_ENCODED_HEADER.name()) => {
if !self.flags.is_streaming_request_body {
continue;
}
// We don't want to override chunked encoding header if it was set by the user
if will_append {
add_transfer_encoding = false;
Expand Down Expand Up @@ -2504,6 +2518,11 @@ impl<'a> HTTPClient<'a> {
self.complete_connecting_process();
return;
}
if self.has_tls_options_unsupported_by_h3() {
self.fail(err!(HTTP3Unsupported));
self.complete_connecting_process();
return;
}
// SAFETY: runs on the HTTP thread after `HTTPThread::init` set
// `uws_loop` to its live `us_loop_t`.
let Some(ctx) = h3::ClientContext::get_or_create(unsafe {
Expand Down
18 changes: 18 additions & 0 deletions src/install/PackageInstaller.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1382,6 +1382,24 @@ impl<'a> PackageInstaller<'a> {
installer.cache_dir = Fd::cwd();
} else {
// transitive folder dependencies are relative to their parent. they are not hoisted
if folder.len() >= self.folder_path_buf.len()
|| bin::bin_target_escapes_package_dir(folder)
{
if log_level != Options::LogLevel::Silent {
Output::pretty_errorln(format_args!(
"<r><red>error<r>: refusing to install dependency <b>{}<r> with unsafe folder path \"{}\"",
bstr::BStr::new(pkg_name.slice(string_buf!())),
bstr::BStr::new(folder),
));
}
self.summary.fail += 1;
self.increment_tree_install_count(
!IS_PENDING_PACKAGE_INSTALL,
self.current_tree_id,
log_level,
);
return;
}
self.folder_path_buf[..folder.len()].copy_from_slice(folder);
self.folder_path_buf[folder.len()] = 0;
// SAFETY: buf[folder.len()] == 0 written above
Expand Down
5 changes: 1 addition & 4 deletions src/install/PackageManager/PackageManagerDirectories.rs
Original file line number Diff line number Diff line change
Expand Up @@ -581,10 +581,7 @@ impl<'a> ByteCursor<'a> {
#[inline(always)]
fn finish_z(self) -> &'a ZStr {
let at = self.at;
debug_assert!(at < self.buf.len());
// SAFETY: see `put`; one byte of headroom for the NUL is part of the
// PathBuffer-size invariant.
unsafe { *self.buf.as_mut_ptr().add(at) = 0 };
self.buf[at] = 0;
ZStr::from_buf(self.buf, at)
}
}
Expand Down
4 changes: 4 additions & 0 deletions src/install/PackageManager/PackageManagerEnqueue.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2628,6 +2628,10 @@ fn get_or_put_resolved_package(
}

// transitive folder dependencies do not have their dependencies resolved
if crate::bin::bin_target_escapes_package_dir(this.lockfile.str(&folder)) {
break 'res FolderResolutionValue::Err(bun_core::err!("MissingPackageJSON"));
}

let mut package = Package::default();

{
Expand Down
38 changes: 29 additions & 9 deletions src/install/TarballStream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ pub struct TarballStream {
bytes_received: usize,
entry_count: u32,
fail: Option<bun_core::Error>,
invalid_name: bool,

/// Thread-pool task that runs `drain`. Re-enqueued whenever new data
/// arrives and no drain is currently in flight.
Expand Down Expand Up @@ -249,6 +250,7 @@ impl TarballStream {
bytes_received: 0,
entry_count: 0,
fail: None,
invalid_name: false,
drain_task: thread_pool::Task {
node: thread_pool::Node::default(),
callback: drain_callback,
Expand Down Expand Up @@ -663,6 +665,13 @@ impl TarballStream {
// Tag::Extract` for streaming tarballs).
let tarball = &self.extract_task.request_extract().tarball;
let (_, basename) = tarball.name_and_basename();
if !tarball.resolution.tag.is_git()
&& tarball.resolution.tag != ResolutionTag::LocalTarball
&& !crate::dependency::is_safe_install_folder_name(&basename[0..basename.len().min(32)])
{
self.invalid_name = true;
return Err(bun_core::err!("InstallFailed"));
}
Comment thread
Jarred-Sumner marked this conversation as resolved.
let mut buf = PathBuffer::uninit();
let tmpname = FileSystem::tmpname(
&basename[0..basename.len().min(32)],
Expand Down Expand Up @@ -1052,15 +1061,26 @@ impl TarballStream {
};

if let Some(err) = self.fail {
(*task).log.add_error_fmt(
None,
bun_ast::Loc::EMPTY,
format_args!(
"{} extracting tarball for \"{}\"",
err.name(),
bstr::BStr::new(tarball.name.slice()),
),
);
if self.invalid_name {
(*task).log.add_error_fmt(
None,
bun_ast::Loc::EMPTY,
format_args!(
"Refusing to install package with invalid name \"{}\"",
bun_fmt::s(tarball.name_and_basename().0),
),
);
} else {
(*task).log.add_error_fmt(
None,
bun_ast::Loc::EMPTY,
format_args!(
"{} extracting tarball for \"{}\"",
err.name(),
bstr::BStr::new(tarball.name.slice()),
),
);
}
(*task).err = Some(err);
(*task).status = TaskStatus::Fail;
return;
Expand Down
Loading
Loading