diff --git a/packages/bun-uws/src/HttpParser.h b/packages/bun-uws/src/HttpParser.h index e50745982a589..59bfd0f76935e 100644 --- a/packages/bun-uws/src/HttpParser.h +++ b/packages/bun-uws/src/HttpParser.h @@ -39,6 +39,8 @@ #include "ProxyParser.h" #include "QueryParser.h" +extern "C" size_t BUN_DEFAULT_MAX_HTTP_HEADER_SIZE; + namespace uWS { @@ -207,7 +209,7 @@ namespace uWS /* This guy really has only 30 bits since we reserve two highest bits to chunked encoding parsing state */ uint64_t remainingStreamingBytes = 0; - const size_t MAX_FALLBACK_SIZE = 1024 * 8; + const size_t MAX_FALLBACK_SIZE = BUN_DEFAULT_MAX_HTTP_HEADER_SIZE; /* Returns UINT_MAX on error. Maximum 999999999 is allowed. */ static uint64_t toUnsignedInteger(std::string_view str) { diff --git a/src/bun.js/node/node_http_binding.zig b/src/bun.js/node/node_http_binding.zig index a4f9830756029..7ad2be8088e53 100644 --- a/src/bun.js/node/node_http_binding.zig +++ b/src/bun.js/node/node_http_binding.zig @@ -37,3 +37,24 @@ pub fn getBunServerAllClosedPromise(globalThis: *JSC.JSGlobalObject, callframe: return globalThis.throwInvalidArgumentTypeValue("server", "bun.Server", value); } + +pub fn getMaxHTTPHeaderSize(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) callconv(JSC.conv) JSC.JSValue { + _ = globalThis; // autofix + _ = callframe; // autofix + return JSC.JSValue.jsNumber(bun.http.max_http_header_size); +} + +pub fn setMaxHTTPHeaderSize(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) callconv(JSC.conv) JSC.JSValue { + const arguments = callframe.arguments(1).slice(); + if (arguments.len < 1) { + globalThis.throwNotEnoughArguments("setMaxHTTPHeaderSize", 1, arguments.len); + return .zero; + } + const value = arguments[0]; + const num = value.coerceToInt64(globalThis); + if (num <= 0) { + return globalThis.throwInvalidArgumentTypeValue("maxHeaderSize", "non-negative integer", value); + } + bun.http.max_http_header_size = @intCast(num); + return JSC.JSValue.jsNumber(bun.http.max_http_header_size); +} diff --git a/src/cli.zig b/src/cli.zig index c85bfb3ea325a..7d6b2ae3896be 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -209,6 +209,7 @@ pub const Arguments = struct { clap.parseParam("-u, --origin ") catch unreachable, clap.parseParam("--conditions ... Pass custom conditions to resolve") catch unreachable, clap.parseParam("--fetch-preconnect ... Preconnect to a URL while code is loading") catch unreachable, + clap.parseParam("--max-http-header-size Set the maximum size of HTTP headers in bytes. Default is 16KiB") catch unreachable, }; const auto_or_run_params = [_]ParamType{ @@ -612,6 +613,18 @@ pub const Arguments = struct { } } + if (args.option("--max-http-header-size")) |size_str| { + const size = std.fmt.parseInt(usize, size_str, 10) catch { + Output.errGeneric("Invalid value for --max-http-header-size: \"{s}\". Must be a positive integer\n", .{size_str}); + Global.exit(1); + }; + if (size == 0) { + bun.http.max_http_header_size = 1024 * 1024 * 1024; + } else { + bun.http.max_http_header_size = size; + } + } + ctx.debug.offline_mode_setting = if (args.flag("--prefer-offline")) Bunfig.OfflineMode.offline else if (args.flag("--prefer-latest")) diff --git a/src/http.zig b/src/http.zig index 099b82d44630d..bda215c4cacec 100644 --- a/src/http.zig +++ b/src/http.zig @@ -53,6 +53,11 @@ var async_http_id: std.atomic.Value(u32) = std.atomic.Value(u32).init(0); const MAX_REDIRECT_URL_LENGTH = 128 * 1024; var custom_ssl_context_map = std.AutoArrayHashMap(*SSLConfig, *NewHTTPContext(true)).init(bun.default_allocator); +pub var max_http_header_size: usize = 16 * 1024; +comptime { + @export(max_http_header_size, .{ .name = "BUN_DEFAULT_MAX_HTTP_HEADER_SIZE" }); +} + const print_every = 0; var print_every_i: usize = 0; diff --git a/src/js/node/http.ts b/src/js/node/http.ts index 4bb8ee17cae8b..1e879ea355f80 100644 --- a/src/js/node/http.ts +++ b/src/js/node/http.ts @@ -2250,6 +2250,9 @@ function emitAbortNextTick(self) { self.emit("abort"); } +const setMaxHTTPHeaderSize = $newZigFunction("node_http_binding.zig", "setMaxHTTPHeaderSize", 1); +const getMaxHTTPHeaderSize = $newZigFunction("node_http_binding.zig", "getMaxHTTPHeaderSize", 0); + var globalAgent = new Agent(); export default { Agent, @@ -2261,7 +2264,12 @@ export default { IncomingMessage, request, get, - maxHeaderSize: 16384, + get maxHeaderSize() { + return getMaxHTTPHeaderSize(); + }, + set maxHeaderSize(value) { + setMaxHTTPHeaderSize(value); + }, validateHeaderName, validateHeaderValue, setMaxIdleHTTPParsers(max) { diff --git a/test/js/node/http/max-header-size-fixture.ts b/test/js/node/http/max-header-size-fixture.ts new file mode 100644 index 0000000000000..33f4af5ec34ed --- /dev/null +++ b/test/js/node/http/max-header-size-fixture.ts @@ -0,0 +1,33 @@ +import http from "node:http"; + +if (http.maxHeaderSize !== parseInt(process.env.BUN_HTTP_MAX_HEADER_SIZE, 10)) { + throw new Error("BUN_HTTP_MAX_HEADER_SIZE is not set to the correct value"); +} + +using server = Bun.serve({ + port: 0, + fetch(req) { + return new Response(JSON.stringify(req.headers, null, 2)); + }, +}); + +await fetch(`${server.url}/`, { + headers: { + "Huge": Buffer.alloc(Math.max(http.maxHeaderSize, 256) - 256, "abc").toString(), + }, +}); + +try { + await fetch(`${server.url}/`, { + headers: { + "Huge": Buffer.alloc(http.maxHeaderSize + 1024, "abc").toString(), + }, + }); + throw new Error("bad"); +} catch (e) { + if (e.message.includes("bad")) { + process.exit(1); + } + + process.exit(0); +} diff --git a/test/js/node/http/node-http-maxHeaderSize.test.ts b/test/js/node/http/node-http-maxHeaderSize.test.ts new file mode 100644 index 0000000000000..fd0c3594da139 --- /dev/null +++ b/test/js/node/http/node-http-maxHeaderSize.test.ts @@ -0,0 +1,83 @@ +import http from "node:http"; +import path from "path"; +import { test, expect } from "bun:test"; +import { bunEnv } from "harness"; + +test("maxHeaderSize", async () => { + const originalMaxHeaderSize = http.maxHeaderSize; + expect(http.maxHeaderSize).toBe(16 * 1024); + // @ts-expect-error its a liar + http.maxHeaderSize = 1024; + expect(http.maxHeaderSize).toBe(1024); + { + using server = Bun.serve({ + port: 0, + + fetch(req) { + return new Response(JSON.stringify(req.headers, null, 2)); + }, + }); + + expect( + async () => + await fetch(`${server.url}/`, { + headers: { + "Huge": Buffer.alloc(8 * 1024, "abc").toString(), + }, + }), + ).toThrow(); + expect( + async () => + await fetch(`${server.url}/`, { + headers: { + "Huge": Buffer.alloc(512, "abc").toString(), + }, + }), + ).not.toThrow(); + } + http.maxHeaderSize = 16 * 1024; + { + using server = Bun.serve({ + port: 0, + + fetch(req) { + return new Response(JSON.stringify(req.headers, null, 2)); + }, + }); + + expect( + async () => + await fetch(`${server.url}/`, { + headers: { + "Huge": Buffer.alloc(15 * 1024, "abc").toString(), + }, + }), + ).not.toThrow(); + expect( + async () => + await fetch(`${server.url}/`, { + headers: { + "Huge": Buffer.alloc(17 * 1024, "abc").toString(), + }, + }), + ).toThrow(); + } + + http.maxHeaderSize = originalMaxHeaderSize; +}); + +test("--max-http-header-size=1024", async () => { + const size = 1024; + bunEnv.BUN_HTTP_MAX_HEADER_SIZE = size; + expect(["--max-http-header-size=" + size, path.join(import.meta.dir, "max-header-size-fixture.ts")]).toRun(); +}); + +test("--max-http-header-size=NaN", async () => { + expect(["--max-http-header-size=" + "NaN", path.join(import.meta.dir, "max-header-size-fixture.ts")]).not.toRun(); +}); + +test("--max-http-header-size=16*1024", async () => { + const size = 16 * 1024; + bunEnv.BUN_HTTP_MAX_HEADER_SIZE = size; + expect(["--max-http-header-size=" + size, path.join(import.meta.dir, "max-header-size-fixture.ts")]).toRun(); +});