Compare commits

..

1 Commits

Author SHA1 Message Date
Claude Bot
cdc636c964 fix(http): add missing header validation in appendHeader
appendHeader only called validateString on the header name, bypassing
the validateHeaderName and validateHeaderValue checks that setHeader
uses. This allowed invalid HTTP token characters in header names and
undefined values to pass through. Add the same validation and
headers-already-sent check that setHeader and Node.js use.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-12 04:41:09 +00:00
5 changed files with 68 additions and 47 deletions

View File

@@ -202,7 +202,11 @@ const OutgoingMessagePrototype = {
_closed: false,
_headerNames: undefined,
appendHeader(name, value) {
validateString(name, "name");
if ((this._header !== undefined && this._header !== null) || this[headerStateSymbol] === NodeHTTPHeaderState.sent) {
throw $ERR_HTTP_HEADERS_SENT("append");
}
validateHeaderName(name);
validateHeaderValue(name, value);
var headers = (this[headersSymbol] ??= new Headers());
headers.append(name, value);
return this;

View File

@@ -34,11 +34,6 @@ pub fn main() void {
// This should appear before we make any calls at all to libuv.
// So it's safest to put it very early in the main function.
if (Environment.isWindows) {
// Set the Windows timer resolution to 1ms. Without this, the default
// resolution is ~15.6ms which causes timers like setInterval(fn, 16)
// to fire at ~28ms intervals instead of ~16ms. (See #26965)
_ = _bun.windows.timeBeginPeriod(1);
_ = _bun.windows.libuv.uv_replace_allocator(
&_bun.mimalloc.mi_malloc,
&_bun.mimalloc.mi_realloc,

View File

@@ -86,9 +86,6 @@ pub const WPathBuffer = if (Environment.isWindows) bun.WPathBuffer else void;
pub const HANDLE = win32.HANDLE;
pub const HMODULE = win32.HMODULE;
/// https://learn.microsoft.com/en-us/windows/win32/api/timeapi/nf-timeapi-timebeginperiod
pub extern "winmm" fn timeBeginPeriod(uPeriod: UINT) callconv(.winapi) UINT;
/// https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getfileinformationbyhandle
pub extern "kernel32" fn GetFileInformationByHandle(
hFile: HANDLE,

View File

@@ -1031,6 +1031,69 @@ describe("node:http", () => {
expect(() => validateHeaderValue("Foo", "Bar\r")).toThrow();
});
test("appendHeader validates header name and value like setHeader", async () => {
await using server = createServer((req, res) => {
res.end("ok");
});
server.listen({ port: 0 });
await once(server, "listening");
const port = (server.address() as AddressInfo).port;
const req = request({ port, method: "GET", path: "/" });
// Invalid header names should throw ERR_INVALID_HTTP_TOKEN
expect(() => req.appendHeader("invalid header", "value")).toThrow(
expect.objectContaining({ code: "ERR_INVALID_HTTP_TOKEN" }),
);
expect(() => req.appendHeader("x(test)", "value")).toThrow(
expect.objectContaining({ code: "ERR_INVALID_HTTP_TOKEN" }),
);
expect(() => req.appendHeader("", "value")).toThrow(expect.objectContaining({ code: "ERR_INVALID_HTTP_TOKEN" }));
expect(() => req.appendHeader("x:test", "value")).toThrow(
expect.objectContaining({ code: "ERR_INVALID_HTTP_TOKEN" }),
);
// Undefined value should throw ERR_HTTP_INVALID_HEADER_VALUE
expect(() => req.appendHeader("x-test", undefined as any)).toThrow(
expect.objectContaining({ code: "ERR_HTTP_INVALID_HEADER_VALUE" }),
);
// CRLF in value should throw ERR_INVALID_CHAR
expect(() => req.appendHeader("x-test", "value\r\ninjected: true")).toThrow(
expect.objectContaining({ code: "ERR_INVALID_CHAR" }),
);
// Null byte in value should throw ERR_INVALID_CHAR
expect(() => req.appendHeader("x-test", "val\x00ue")).toThrow(
expect.objectContaining({ code: "ERR_INVALID_CHAR" }),
);
// Valid headers should work
req.appendHeader("x-valid", "value");
req.appendHeader("x-valid", "another-value");
req.end();
await once(req, "response");
});
test("appendHeader throws after headers sent", async () => {
const { promise, resolve } = Promise.withResolvers<void>();
await using server = createServer((req, res) => {
res.write("data");
expect(() => res.appendHeader("x-late", "value")).toThrow(
expect.objectContaining({ code: "ERR_HTTP_HEADERS_SENT" }),
);
res.end();
resolve();
});
server.listen({ port: 0 });
await once(server, "listening");
const port = (server.address() as AddressInfo).port;
const res = await fetch("http://localhost:" + port);
await res.text();
await promise;
});
test("req.req = req", done => {
const server = createServer((req, res) => {
req.req = req;

View File

@@ -1,38 +0,0 @@
import { expect, test } from "bun:test";
// https://github.com/oven-sh/bun/issues/26965
// setInterval(fn, 16) fires with ~28ms intervals on Windows instead of ~16ms
// due to the default Windows timer resolution being ~15.6ms.
// The fix calls timeBeginPeriod(1) at startup to set 1ms resolution.
test("setInterval fires at approximately the requested interval", async () => {
const interval = 16;
const count = 50;
const times: number[] = [];
let last = performance.now();
await new Promise<void>(resolve => {
let i = 0;
const id = setInterval(() => {
const now = performance.now();
times.push(now - last);
last = now;
i++;
if (i >= count) {
clearInterval(id);
resolve();
}
}, interval);
});
// Drop the first few measurements as they can be noisy during startup
const stable = times.slice(5);
const avg = stable.reduce((a, b) => a + b, 0) / stable.length;
// The average interval should be close to the requested 16ms.
// Before the fix on Windows, this was ~28ms (nearly 2x).
// Allow up to 22ms to account for normal scheduling jitter,
// but catch the ~28ms+ intervals caused by 15.6ms timer resolution.
expect(avg).toBeLessThan(22);
expect(avg).toBeGreaterThan(10);
});