Skip to content
Merged
Show file tree
Hide file tree
Changes from 19 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 scripts/verify-baseline-static/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,14 @@ fn is_harmless_on_nehalem(insn: &Instruction) -> bool {
return true;
}

// CLDEMOTE encodes in hint/NOP space (0f 1c /0) and is architecturally
// treated as a NOP on CPUs that don't enumerate it (SDM vol. 2A). Newer
// UCRT string routines (e.g. strpbrk) emit it unconditionally as a cache
// hint; on Nehalem it NOPs and the routine behaves identically.
if insn.mnemonic() == Mnemonic::Cldemote {
return true;
}

false
}

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
149 changes: 142 additions & 7 deletions src/jsc/bindings/NodeHTTP.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
#include "ZigGeneratedClasses.h"
#include <JavaScriptCore/LazyPropertyInlines.h>
#include <JavaScriptCore/VMTrapsInlines.h>
#include <wtf/text/StringBuilder.h>
#include "JSSocketAddressDTO.h"
#include "node/JSNodeHTTPServerSocket.h"
#include "node/JSNodeHTTPServerSocketPrototype.h"
Expand Down Expand Up @@ -110,6 +111,82 @@ static EncodedJSValue assignHeadersFromFetchHeaders(FetchHeaders& impl, JSObject
return JSValue::encode(tuple);
}

enum class RequestHeaderKind : uint8_t {
Joinable,
Singleton,
Cookie,
SetCookie,
};

static RequestHeaderKind requestHeaderKind(WebCore::HTTPHeaderName name)
{
switch (name) {
case WebCore::HTTPHeaderName::SetCookie:
return RequestHeaderKind::SetCookie;
case WebCore::HTTPHeaderName::Cookie:
return RequestHeaderKind::Cookie;
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 RequestHeaderKind::Singleton;
default:
return RequestHeaderKind::Joinable;
}
}

static RequestHeaderKind requestHeaderKind(const WTF::String& lowercasedName)
{
if (lowercasedName == "from"_s || lowercasedName == "max-forwards"_s || lowercasedName == "retry-after"_s || lowercasedName == "server"_s)
return RequestHeaderKind::Singleton;
return RequestHeaderKind::Joinable;
}

// Called after putDirect reported PutPropertySlot::ExistingProperty for a
// request header name. The first pass stores every header with a single put,
// so by the time a duplicate is detected the earlier value has already been
// replaced. Re-derive the value Node.js would produce for this name from the
// raw header list: the first value wins for singleton headers, Cookie joins
// with "; ", and everything else joins with ", ". Names that map to the same
// property key are exactly the names that are ASCII-case-insensitively equal.
static JSString* duplicateRequestHeaderValue(uWS::HttpRequest* request, StringView nameView, RequestHeaderKind kind, JSC::JSGlobalObject* globalObject, JSC::VM& vm)
{
auto scope = DECLARE_THROW_SCOPE(vm);

WTF::StringBuilder builder;
ASCIILiteral separator = kind == RequestHeaderKind::Cookie ? "; "_s : ", "_s;
bool seenAny = false;

for (auto it = request->begin(); it != request->end(); ++it) {
auto pair = *it;
StringView candidateName(std::span { reinterpret_cast<const Latin1Character*>(pair.first.data()), pair.first.length() });
if (!equalIgnoringASCIICase(candidateName, nameView))
continue;
if (seenAny)
builder.append(separator);
seenAny = true;
builder.append(StringView(std::span { reinterpret_cast<const Latin1Character*>(pair.second.data()), pair.second.length() }));
if (kind == RequestHeaderKind::Singleton)
break;
}

if (builder.hasOverflowed()) [[unlikely]] {
throwOutOfMemoryError(globalObject, scope);
return nullptr;
}
return jsString(vm, builder.toString());
}

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 @@ -143,8 +220,6 @@ static void assignHeadersFromUWebSocketsForCall(uWS::HttpRequest* request, JSVal
MarkedArgumentBuffer arrayValues;
HTTPHeaderIdentifiers& identifiers = WebCore::clientData(vm)->httpHeaderIdentifiers();

args.append(headersObject);

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() });
Expand All @@ -159,20 +234,23 @@ static void assignHeadersFromUWebSocketsForCall(uWS::HttpRequest* request, JSVal

Identifier nameIdentifier;
JSString* nameString = nullptr;
WTF::String lowercasedName;
// `findHTTPHeaderName` only writes `name` when it returns true, so the
// SetCookie check must be gated on a successful lookup rather than on the
// (otherwise indeterminate) `name` value. set-cookie is always a known
// header name, so an unrecognized header is never set-cookie.
bool knownHeader = WebCore::findHTTPHeaderName(nameView, name);
bool isSetCookie = false;

if (WebCore::findHTTPHeaderName(nameView, name)) {
if (knownHeader) {
nameString = identifiers.stringFor(globalObject, name);
nameIdentifier = identifiers.identifierFor(vm, name);
isSetCookie = name == WebCore::HTTPHeaderName::SetCookie;
} else {
WTF::String wtfString = nameView.toString();
nameString = jsString(vm, wtfString);
nameIdentifier = Identifier::fromString(vm, wtfString.convertToASCIILowercase());
lowercasedName = wtfString.convertToASCIILowercase();
nameIdentifier = Identifier::fromString(vm, lowercasedName);
}

if (isSetCookie) {
Expand All @@ -189,14 +267,42 @@ static void assignHeadersFromUWebSocketsForCall(uWS::HttpRequest* request, JSVal
RETURN_IF_EXCEPTION(scope, void());

} else {
headersObject->putDirectMayBeIndex(globalObject, nameIdentifier, jsValue);
if (std::optional<uint32_t> index = parseIndex(nameIdentifier)) [[unlikely]] {
// Index-shaped names can't report back through PutPropertySlot,
// so check for an existing value directly. A numeric name is
// never a known header name, so duplicates always comma-join.
JSValue existing = headersObject->getDirectIndex(globalObject, index.value());
RETURN_IF_EXCEPTION(scope, void());
JSValue valueToPut = jsValue;
if (existing) {
valueToPut = jsString(globalObject, asString(existing), jsString(vm, String(", "_s)), jsValue);
RETURN_IF_EXCEPTION(scope, void());
}
headersObject->putDirectIndex(globalObject, index.value(), valueToPut);
} else {
PutPropertySlot slot(headersObject);
headersObject->putDirect(vm, nameIdentifier, jsValue, 0, slot);
if (slot.type() == PutPropertySlot::ExistingProperty) [[unlikely]] {
// Duplicate header name. putDirect already located the
// property and replaced its value, and the slot recorded
// the offset it lives at, so apply Node's merge rules in
// place without looking the name up again.
RequestHeaderKind kind = knownHeader ? requestHeaderKind(name) : requestHeaderKind(lowercasedName);
JSString* merged = duplicateRequestHeaderValue(request, nameView, kind, globalObject, vm);
RETURN_IF_EXCEPTION(scope, void());
headersObject->structure()->didReplaceProperty(slot.cachedOffset());
headersObject->putDirectOffset(vm, slot.cachedOffset(), merged);
}
}
RETURN_IF_EXCEPTION(scope, void());
arrayValues.append(nameString);
arrayValues.append(jsValue);
RETURN_IF_EXCEPTION(scope, void());
}
}

args.append(headersObject);

JSC::JSArray* array;
{

Expand Down Expand Up @@ -347,9 +453,10 @@ static EncodedJSValue assignHeadersFromUWebSockets(uWS::HttpRequest* request, JS
HTTPHeaderName name;
WTF::String nameString;
WTF::String lowercasedNameString;
bool knownHeader = WebCore::findHTTPHeaderName(nameView, name);
bool isSetCookie = false;

if (WebCore::findHTTPHeaderName(nameView, name)) {
if (knownHeader) {
nameString = WTF::httpHeaderNameStringImpl(name);
lowercasedNameString = nameString;
isSetCookie = name == WebCore::HTTPHeaderName::SetCookie;
Expand All @@ -374,7 +481,35 @@ 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 (std::optional<uint32_t> index = parseIndex(nameIdentifier)) [[unlikely]] {
// Index-shaped names can't report back through PutPropertySlot,
// so check for an existing value directly. A numeric name is
// never a known header name, so duplicates always comma-join.
JSValue existing = headersObject->getDirectIndex(globalObject, index.value());
RETURN_IF_EXCEPTION(scope, {});
JSValue valueToPut = jsValue;
if (existing) {
valueToPut = jsString(globalObject, asString(existing), jsString(vm, String(", "_s)), jsValue);
RETURN_IF_EXCEPTION(scope, {});
}
headersObject->putDirectIndex(globalObject, index.value(), valueToPut);
} else {
PutPropertySlot slot(headersObject);
headersObject->putDirect(vm, nameIdentifier, jsValue, 0, slot);
if (slot.type() == PutPropertySlot::ExistingProperty) [[unlikely]] {
// Duplicate header name. putDirect already located the
// property and replaced its value, and the slot recorded
// the offset it lives at, so apply Node's merge rules in
// place without looking the name up again.
RequestHeaderKind kind = knownHeader ? requestHeaderKind(name) : requestHeaderKind(lowercasedNameString);
JSString* merged = duplicateRequestHeaderValue(request, nameView, kind, globalObject, vm);
RETURN_IF_EXCEPTION(scope, {});
headersObject->structure()->didReplaceProperty(slot.cachedOffset());
headersObject->putDirectOffset(vm, slot.cachedOffset(), merged);
}
}
RETURN_IF_EXCEPTION(scope, {});
array->putDirectIndex(globalObject, i++, jsString(vm, nameString));
array->putDirectIndex(globalObject, i++, jsValue);
RETURN_IF_EXCEPTION(scope, {});
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
Loading
Loading