Compare commits

...

1 Commits

Author SHA1 Message Date
Claude Bot
b5b5d4564b fix(http): validate header names and values in writeEarlyHints
The writeEarlyHints method constructed raw HTTP 103 response bytes by
concatenating user-provided header keys and values without validation.
For non-'link' keys, CRLF sequences in names or values could inject
arbitrary HTTP headers. Apply validateHeaderName and validateHeaderValue
checks (the same ones used by setHeader) before writing to the socket.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-12 04:54:50 +00:00
2 changed files with 146 additions and 2 deletions

View File

@@ -1,7 +1,11 @@
// Hardcoded module "node:_http_server"
const EventEmitter: typeof import("node:events").EventEmitter = require("node:events");
const { Duplex, Stream } = require("node:stream");
const { _checkInvalidHeaderChar: checkInvalidHeaderChar } = require("node:_http_common");
const {
_checkInvalidHeaderChar: checkInvalidHeaderChar,
validateHeaderName,
validateHeaderValue,
} = require("node:_http_common");
const { validateObject, validateLinkHeaderValue, validateBoolean, validateInteger } = require("internal/validators");
const { ConnResetException } = require("internal/shared");
@@ -1284,7 +1288,10 @@ ServerResponse.prototype.writeEarlyHints = function (hints, cb) {
for (const key of ObjectKeys(hints)) {
if (key !== "link") {
head += key + ": " + hints[key] + "\r\n";
const value = hints[key];
validateHeaderName(key);
validateHeaderValue(key, value);
head += key + ": " + value + "\r\n";
}
}

View File

@@ -0,0 +1,137 @@
import { describe, expect, test } from "bun:test";
import { bunEnv, bunExe } from "harness";
describe("writeEarlyHints", () => {
test("rejects CRLF injection in header name", async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`
const http = require("node:http");
const server = http.createServer((req, res) => {
try {
res.writeEarlyHints({
link: "</style.css>; rel=preload",
"x-custom\\r\\nSet-Cookie: session=evil\\r\\nX-Injected": "val",
});
console.log("FAIL: no error thrown");
process.exit(1);
} catch (e) {
console.log("error_code:" + e.code);
res.writeHead(200);
res.end("ok");
}
});
server.listen(0, () => {
http.get({ port: server.address().port }, (res) => {
let data = "";
res.on("data", (c) => data += c);
res.on("end", () => {
console.log("body:" + data);
server.close();
});
});
});
`,
],
env: bunEnv,
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout).toContain("error_code:ERR_INVALID_HTTP_TOKEN");
expect(stdout).toContain("body:ok");
expect(exitCode).toBe(0);
});
test("rejects CRLF injection in header value", async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`
const http = require("node:http");
const server = http.createServer((req, res) => {
try {
res.writeEarlyHints({
link: "</style.css>; rel=preload",
"x-custom": "legitimate\\r\\nSet-Cookie: session=evil",
});
console.log("FAIL: no error thrown");
process.exit(1);
} catch (e) {
console.log("error_code:" + e.code);
res.writeHead(200);
res.end("ok");
}
});
server.listen(0, () => {
http.get({ port: server.address().port }, (res) => {
let data = "";
res.on("data", (c) => data += c);
res.on("end", () => {
console.log("body:" + data);
server.close();
});
});
});
`,
],
env: bunEnv,
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout).toContain("error_code:ERR_INVALID_CHAR");
expect(stdout).toContain("body:ok");
expect(exitCode).toBe(0);
});
test("allows valid non-link headers in early hints", async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`
const http = require("node:http");
const server = http.createServer((req, res) => {
try {
res.writeEarlyHints({
link: "</style.css>; rel=preload",
"x-custom": "valid-value",
"x-another": "also-valid",
});
console.log("OK: no error");
res.writeHead(200);
res.end("ok");
} catch (e) {
console.log("FAIL: " + e.message);
process.exit(1);
}
});
server.listen(0, () => {
http.get({ port: server.address().port }, (res) => {
let data = "";
res.on("data", (c) => data += c);
res.on("end", () => {
console.log("body:" + data);
server.close();
});
});
});
`,
],
env: bunEnv,
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout).toContain("OK: no error");
expect(stdout).toContain("body:ok");
expect(exitCode).toBe(0);
});
});