Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
10843fe
transpiler: tighten cached module metadata validation
Jarred-Sumner May 30, 2026
e4d9ec0
postgres: bound protocol message handling
Jarred-Sumner May 30, 2026
5566039
htmlrewriter: tighten comment mutation handling
Jarred-Sumner May 30, 2026
9456499
test: add regression coverage for input validation changes
Jarred-Sumner May 30, 2026
08bb3a6
bun-lambda: build event request URLs from the request context domain
Jarred-Sumner May 30, 2026
e125f62
create: pass detected dependencies as positional install arguments
Jarred-Sumner May 30, 2026
01f91d6
node:zlib: tighten write state handling
Jarred-Sumner May 30, 2026
d0b16a8
node:net: tighten subnet rule matching
Jarred-Sumner May 30, 2026
18b747a
node:http: bound duplicate header handling
Jarred-Sumner May 30, 2026
a4b7c5b
glob: tighten cwd validation
Jarred-Sumner May 30, 2026
a6f3f97
test: add regression coverage for input validation changes
Jarred-Sumner May 30, 2026
a081a35
Merge branch 'claude/security-round-10-shard-2' into claude/security-…
Jarred-Sumner May 30, 2026
ec0c034
Merge branch 'claude/security-round-10-shard-3' into claude/security-…
Jarred-Sumner May 30, 2026
2f94449
[autofix.ci] apply automated fixes
autofix-ci[bot] May 30, 2026
493d444
create: build install argv with vec macro
Jarred-Sumner May 30, 2026
e005625
test: tighten assertions and platform coverage in new regression tests
Jarred-Sumner May 30, 2026
3e0ef3d
node:http: restructure request header object construction
Jarred-Sumner Jun 1, 2026
a35bf0d
verify-baseline: treat hint-space cldemote as a nop
Jarred-Sumner Jun 1, 2026
1ffc4ca
node:http: merge duplicate request headers in place via PutPropertySl…
robobun Jun 2, 2026
a82c15d
ci: retrigger
robobun Jun 2, 2026
054a9fc
node:http: check for existing header before storing, join as flat str…
robobun Jun 2, 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
12 changes: 7 additions & 5 deletions packages/bun-lambda/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -320,9 +320,10 @@ function formatHttpEventV1(event: HttpEventV1): Request {
headers.append(name, value);
}
}
const hostname = headers.get("Host") ?? request.domainName;
const hostname = request.domainName ?? headers.get("Host");
const proto = headers.get("X-Forwarded-Proto") ?? "http";
const url = new URL(request.path, `${proto}://${hostname}/`);
const url = new URL(`${proto}://${hostname}/`);
url.pathname = request.path;
for (const [name, values] of Object.entries(event.multiValueQueryStringParameters ?? {})) {
for (const value of values ?? []) {
url.searchParams.append(name, value);
Expand Down Expand Up @@ -367,9 +368,10 @@ function formatHttpEventV2(event: HttpEventV2): Request {
for (const cookie of event.cookies ?? []) {
headers.append("Set-Cookie", cookie);
}
const hostname = headers.get("Host") ?? request.domainName;
const hostname = request.domainName ?? headers.get("Host");
const proto = headers.get("X-Forwarded-Proto") ?? "http";
const url = new URL(request.http.path, `${proto}://${hostname}/`);
const url = new URL(`${proto}://${hostname}/`);
url.pathname = request.http.path;
for (const [name, values] of Object.entries(event.queryStringParameters ?? {})) {
url.searchParams.append(name, values);
}
Expand Down Expand Up @@ -431,7 +433,7 @@ function formatWebSocketUpgrade(event: WebSocketEvent): Request {
headers.append(name, value);
}
}
const hostname = headers.get("Host") ?? request.domainName;
const hostname = request.domainName ?? headers.get("Host");
const proto = headers.get("X-Forwarded-Proto") ?? "http";
const url = new URL(`${proto}://${hostname}/${request.stage}`);
return new Request(url.toString(), {
Expand Down
8 changes: 8 additions & 0 deletions src/bun_core/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,13 @@ impl<T: Copy> Unaligned<T> {

/// Reinterpret `&[Unaligned<T>]` as `&[T]` once the caller has proven
/// `ptr` is naturally aligned (Zig `@alignCast`). Panics in debug if not.
/// Empty slices need no cast: their dangling pointer has align 1, not
/// `align_of::<T>()`, so they are returned as `&[]` directly.
#[inline]
pub fn slice_align_cast(slice: &[Unaligned<T>]) -> &[T] {
if slice.is_empty() {
return &[];
}
debug_assert!(
(slice.as_ptr() as usize).is_multiple_of(core::mem::align_of::<T>()),
"Unaligned::slice_align_cast: pointer is not {}-byte aligned",
Expand All @@ -75,6 +80,9 @@ impl<T: Copy> Unaligned<T> {
/// Mutable counterpart of [`slice_align_cast`].
#[inline]
pub fn slice_align_cast_mut(slice: &mut [Unaligned<T>]) -> &mut [T] {
if slice.is_empty() {
return &mut [];
}
debug_assert!(
(slice.as_ptr() as usize).is_multiple_of(core::mem::align_of::<T>()),
"Unaligned::slice_align_cast_mut: pointer is not {}-byte aligned",
Expand Down
15 changes: 15 additions & 0 deletions src/bundler_jsc/analyze_jsc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,21 @@ pub(crate) extern "C" fn zig__ModuleInfoDeserialized__toJSModuleRecord(
let buffer: &[StringID] = res.buffer();
let record_kinds: &[RecordKind] = res.record_kinds();

let identifier_count = strings_lens.len();
let is_valid_string_id =
|id: StringID| (id.0 as usize) < identifier_count || id.0 >= StringID::STAR_NAMESPACE.0;
if !buffer.iter().copied().all(is_valid_string_id)
|| !requested_modules_keys
.iter()
.copied()
.all(is_valid_string_id)
|| !requested_modules_values
.iter()
.all(|&v| (v.0 as usize) < identifier_count || v.0 >= RequestedModuleValue::Json.0)
{
return core::ptr::null_mut();
}

let identifiers = IdentifierArray::create(strings_lens.len());
// SAFETY: `identifiers` is non-null (returned by `create`); destroyed exactly once at scope exit,
// mirroring Zig's `defer identifiers.destroy()` (runs on both success and early-return paths).
Expand Down
141 changes: 137 additions & 4 deletions src/jsc/bindings/NodeHTTP.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,112 @@ static EncodedJSValue assignHeadersFromFetchHeaders(FetchHeaders& impl, JSObject
return JSValue::encode(tuple);
}

static bool isSingleValueHeader(WebCore::HTTPHeaderName name)
{
switch (name) {
case WebCore::HTTPHeaderName::Age:
case WebCore::HTTPHeaderName::Authorization:
case WebCore::HTTPHeaderName::ContentLength:
case WebCore::HTTPHeaderName::ContentType:
case WebCore::HTTPHeaderName::ETag:
case WebCore::HTTPHeaderName::Expires:
case WebCore::HTTPHeaderName::Host:
case WebCore::HTTPHeaderName::IfModifiedSince:
case WebCore::HTTPHeaderName::IfUnmodifiedSince:
case WebCore::HTTPHeaderName::LastModified:
case WebCore::HTTPHeaderName::Location:
case WebCore::HTTPHeaderName::ProxyAuthorization:
case WebCore::HTTPHeaderName::Referer:
case WebCore::HTTPHeaderName::UserAgent:
return true;
default:
return false;
}
}

static bool isSingleValueHeader(const WTF::String& lowercasedName)
{
return lowercasedName == "from"_s || lowercasedName == "max-forwards"_s || lowercasedName == "retry-after"_s || lowercasedName == "server"_s;
}

static JSC::JSObject* buildHeadersObjectHandlingDuplicates(uWS::HttpRequest* request, JSObject* prototype, JSC::JSGlobalObject* globalObject, JSC::VM& vm)
Comment thread
robobun marked this conversation as resolved.
Outdated
{
auto scope = DECLARE_THROW_SCOPE(vm);

JSC::JSObject* headersObject = JSC::constructEmptyObject(globalObject, prototype, JSFinalObject::defaultInlineCapacity);
Comment thread
robobun marked this conversation as resolved.
Outdated
RETURN_IF_EXCEPTION(scope, nullptr);
JSC::JSArray* setCookiesHeaderArray = nullptr;
HTTPHeaderIdentifiers& identifiers = WebCore::clientData(vm)->httpHeaderIdentifiers();

for (auto it = request->begin(); it != request->end(); ++it) {
auto pair = *it;
StringView nameView = StringView(std::span { reinterpret_cast<const Latin1Character*>(pair.first.data()), pair.first.length() });
std::span<Latin1Character> data;
auto value = String::tryCreateUninitialized(pair.second.length(), data);
if (value.isNull()) [[unlikely]] {
throwOutOfMemoryError(globalObject, scope);
return nullptr;
}
if (pair.second.length() > 0)
memcpy(data.data(), pair.second.data(), pair.second.length());

JSString* jsValue = jsString(vm, value);

HTTPHeaderName name;
Identifier nameIdentifier;
bool isSetCookie = false;
bool isSingleValue = false;
bool isCookie = false;

if (WebCore::findHTTPHeaderName(nameView, name)) {
nameIdentifier = identifiers.identifierFor(vm, name);
isSetCookie = name == WebCore::HTTPHeaderName::SetCookie;
isSingleValue = isSingleValueHeader(name);
isCookie = name == WebCore::HTTPHeaderName::Cookie;
} else {
WTF::String lowercasedNameString = nameView.toString().convertToASCIILowercase();
nameIdentifier = Identifier::fromString(vm, lowercasedNameString);
isSingleValue = isSingleValueHeader(lowercasedNameString);
}

if (isSetCookie) {
if (!setCookiesHeaderArray) {
setCookiesHeaderArray = constructEmptyArray(globalObject, nullptr);
RETURN_IF_EXCEPTION(scope, nullptr);
headersObject->putDirect(vm, nameIdentifier, setCookiesHeaderArray, 0);
RETURN_IF_EXCEPTION(scope, nullptr);
}
setCookiesHeaderArray->push(globalObject, jsValue);
RETURN_IF_EXCEPTION(scope, nullptr);
continue;
}

JSValue existing;
if (std::optional<uint32_t> index = parseIndex(nameIdentifier)) {
existing = headersObject->getDirectIndex(globalObject, index.value());
RETURN_IF_EXCEPTION(scope, nullptr);
} else {
existing = headersObject->getDirect(vm, nameIdentifier);
}

if (!existing) {
headersObject->putDirectMayBeIndex(globalObject, nameIdentifier, jsValue);
RETURN_IF_EXCEPTION(scope, nullptr);
continue;
}

if (isSingleValue)
continue;

JSString* joined = jsString(globalObject, asString(existing), jsString(vm, String(isCookie ? "; "_s : ", "_s)), jsValue);
RETURN_IF_EXCEPTION(scope, nullptr);
headersObject->putDirectMayBeIndex(globalObject, nameIdentifier, joined);
RETURN_IF_EXCEPTION(scope, nullptr);
}

return headersObject;
}

static void assignHeadersFromUWebSocketsForCall(uWS::HttpRequest* request, JSValue methodString, MarkedArgumentBuffer& args, JSC::JSGlobalObject* globalObject, JSC::VM& vm)
{
auto scope = DECLARE_THROW_SCOPE(vm);
Expand Down Expand Up @@ -142,8 +248,7 @@ static void assignHeadersFromUWebSocketsForCall(uWS::HttpRequest* request, JSVal
JSC::JSString* setCookiesHeaderString = nullptr;
MarkedArgumentBuffer arrayValues;
HTTPHeaderIdentifiers& identifiers = WebCore::clientData(vm)->httpHeaderIdentifiers();

args.append(headersObject);
bool sawDuplicateHeader = false;

for (auto it = request->begin(); it != request->end(); ++it) {
auto pair = *it;
Expand Down Expand Up @@ -189,14 +294,27 @@ static void assignHeadersFromUWebSocketsForCall(uWS::HttpRequest* request, JSVal
RETURN_IF_EXCEPTION(scope, void());

} else {
headersObject->putDirectMayBeIndex(globalObject, nameIdentifier, jsValue);
if (parseIndex(nameIdentifier)) [[unlikely]] {
sawDuplicateHeader = true;
headersObject->putDirectMayBeIndex(globalObject, nameIdentifier, jsValue);
Comment thread
robobun marked this conversation as resolved.
Outdated
} else {
PutPropertySlot slot(headersObject);
headersObject->putDirect(vm, nameIdentifier, jsValue, 0, slot);
sawDuplicateHeader |= slot.type() == PutPropertySlot::ExistingProperty;
}
RETURN_IF_EXCEPTION(scope, void());
arrayValues.append(nameString);
arrayValues.append(jsValue);
RETURN_IF_EXCEPTION(scope, void());
}
}

if (sawDuplicateHeader) [[unlikely]] {
headersObject = buildHeadersObjectHandlingDuplicates(request, globalObject->objectPrototype(), globalObject, vm);
RETURN_IF_EXCEPTION(scope, void());
}
args.append(headersObject);

JSC::JSArray* array;
{

Expand Down Expand Up @@ -329,6 +447,7 @@ static EncodedJSValue assignHeadersFromUWebSockets(uWS::HttpRequest* request, JS
RETURN_IF_EXCEPTION(scope, {});
JSC::JSArray* setCookiesHeaderArray = nullptr;
JSC::JSString* setCookiesHeaderString = nullptr;
bool sawDuplicateHeader = false;

unsigned i = 0;

Expand Down Expand Up @@ -374,13 +493,27 @@ static EncodedJSValue assignHeadersFromUWebSockets(uWS::HttpRequest* request, JS
RETURN_IF_EXCEPTION(scope, {});

} else {
headersObject->putDirect(vm, Identifier::fromString(vm, lowercasedNameString), jsValue, 0);
Identifier nameIdentifier = Identifier::fromString(vm, lowercasedNameString);
if (parseIndex(nameIdentifier)) [[unlikely]] {
sawDuplicateHeader = true;
headersObject->putDirectMayBeIndex(globalObject, nameIdentifier, jsValue);
} else {
PutPropertySlot slot(headersObject);
headersObject->putDirect(vm, nameIdentifier, jsValue, 0, slot);
sawDuplicateHeader |= slot.type() == PutPropertySlot::ExistingProperty;
}
RETURN_IF_EXCEPTION(scope, {});
array->putDirectIndex(globalObject, i++, jsString(vm, nameString));
array->putDirectIndex(globalObject, i++, jsValue);
RETURN_IF_EXCEPTION(scope, {});
}
}

if (sawDuplicateHeader) [[unlikely]] {
headersObject = buildHeadersObjectHandlingDuplicates(request, prototype, globalObject, vm);
RETURN_IF_EXCEPTION(scope, {});
}

tuple->putInternalField(vm, 0, headersObject);
tuple->putInternalField(vm, 1, array);

Expand Down
9 changes: 7 additions & 2 deletions src/lolhtml_sys/lol_html.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1091,6 +1091,12 @@ unsafe extern "C" {
content_len: usize,
is_html: bool,
) -> c_int;
fn lol_html_comment_replace(
comment: *mut Comment,
content: *const u8,
content_len: usize,
is_html: bool,
) -> c_int;
safe fn lol_html_comment_remove(comment: &mut Comment);
safe fn lol_html_comment_is_removed(comment: &Comment) -> bool;
safe fn lol_html_comment_source_location_bytes(comment: &Comment) -> SourceLocationBytes;
Expand Down Expand Up @@ -1126,10 +1132,9 @@ impl Comment {

pub fn replace(&mut self, content: &[u8], is_html: bool) -> Result<(), Error> {
auto_disable();
// PORT NOTE: Zig source calls lol_html_comment_before here (likely an upstream bug); ported faithfully
// SAFETY: content ptr/len describe a valid slice
match unsafe {
lol_html_comment_before(self, ptr_without_panic(content), content.len(), is_html)
lol_html_comment_replace(self, ptr_without_panic(content), content.len(), is_html)
} {
0 => Ok(()),
-1 => Err(Error::Fail),
Expand Down
7 changes: 7 additions & 0 deletions src/runtime/api/glob.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,13 @@ impl ScanOpts {
let cwd_str: Box<[u8]> = 'cwd_str: {
let cwd_utf8 = cwd_string.to_utf8_without_ref();

if cwd_utf8.slice().len() > MAX_PATH_BYTES {
return Err(global_this.throw(format_args!(
"{}: invalid `cwd`, longer than {} bytes",
fn_name, MAX_PATH_BYTES
)));
}

// If its absolute return as is
if resolve_path::Platform::AUTO.is_absolute(cwd_utf8.slice()) {
break 'cwd_str Box::<[u8]>::from(cwd_utf8.slice());
Expand Down
2 changes: 1 addition & 1 deletion src/runtime/api/zlib.classes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ function generate(name: string) {
estimatedSize: true,
klass: {},
JSType: "0b11101110",
values: ["writeCallback", "errorCallback", "dictionary", "pendingInput", "pendingOutput"],
values: ["writeCallback", "errorCallback", "dictionary", "pendingInput", "pendingOutput", "writeResult"],

proto: {
init: { fn: "init" },
Expand Down
6 changes: 2 additions & 4 deletions src/runtime/cli/create/SourceFileProjectGenerator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -332,10 +332,7 @@ pub fn generate_files(
}

if !dependencies.is_empty() {
let mut argv: Vec<&[u8]> = Vec::new();
argv.push(b"bun");
argv.push(b"--only-missing");
argv.push(b"install");
let mut argv: Vec<&[u8]> = vec![b"bun", b"--only-missing", b"install", b"--"];
argv.extend(dependencies.iter().map(|d| &d[..]));
run_install(&mut argv)?;
}
Expand All @@ -361,6 +358,7 @@ pub fn generate_files(
shadcn_argv.push(b"--src-dir");
}
shadcn_argv.push(b"-y");
shadcn_argv.push(b"--");
shadcn_argv.extend(components.keys().iter().map(|k| &k[..]));

// print "bun" but use bun.selfExePath()
Expand Down
22 changes: 19 additions & 3 deletions src/runtime/node/net/BlockList.rs
Original file line number Diff line number Diff line change
Expand Up @@ -305,14 +305,17 @@ impl BlockList {
}
Rule::Subnet { network, prefix } => {
if let Some(ip_addr) = address.as_v4() {
if let Some(subnet_addr) = network.as_v4() {
if let Some(subnet_addr) = network.as_sin().map(|s| s.addr) {
if *prefix == 32 {
if ip_addr == subnet_addr {
return Ok(JSValue::TRUE);
} else {
continue;
}
}
if *prefix == 0 {
return Ok(JSValue::TRUE);
}
let one: u32 = 1;
let mask_addr: u32 =
((one << (*prefix as u32)) - 1) << (32 - *prefix as u32);
Expand All @@ -323,8 +326,18 @@ impl BlockList {
}
}
}
if let (Some(addr6), Some(net6)) = (address.as_sin6(), network.as_sin6()) {
let ip_addr: u128 = u128::from_ne_bytes(addr6.addr);
if let Some(net6) = network.as_sin6() {
let ip_addr: u128 = if let Some(addr6) = address.as_sin6() {
u128::from_ne_bytes(addr6.addr)
} else if let Some(ip4) = address.as_v4() {
let mut mapped = [0u8; 16];
mapped[10] = 255;
mapped[11] = 255;
mapped[12..16].copy_from_slice(&ip4.to_ne_bytes());
u128::from_ne_bytes(mapped)
} else {
continue;
};
let subnet_addr: u128 = u128::from_ne_bytes(net6.addr);
if *prefix == 128 {
if ip_addr == subnet_addr {
Expand All @@ -333,6 +346,9 @@ impl BlockList {
continue;
}
}
if *prefix == 0 {
return Ok(JSValue::TRUE);
}
let one: u128 = 1;
let mask_addr = ((one << (*prefix as u32)) - 1) << (128 - *prefix as u32);
let ip_net: u128 = ip_addr.swap_bytes() & mask_addr;
Expand Down
Loading
Loading