How unvetted carriage return characters in HTTP header values allowed attackers to split outgoing HTTP requests in Node.js globalThis.fetch().
Imagine you send a telegraph that says: 'Ship 10 boxes of apples'. But you sneak in special hidden control instructions: 'STOP. IGNORE PREVIOUS. TRANSFER ALL FUNDS TO EVIL CORP'. The telegraph operator reads the message line by line and treats the second line as a brand-new official instruction, sending your money straight to the attacker.
An attacker supplies a crafted header value: Admin\r\nHost: 169.254.169.254.
The Node.js backend calls fetch(url, { headers: { 'X-User': input } }).
Undici serializes the headers without sanitizing \r\n, injecting the forged Host header.
The internal proxy directs the request to the cloud metadata service, exposing AWS/GCP credentials.
Delivered in clean, readable high-level source code (no raw assembly or binary diffs).
// VULNERABLE: lib/core/request.js before patch
function addHeader(headers, key, value) {
// ROOT CAUSE:
// Does not sanitize or reject carriage return (\r) and line feed (\n) in values!
// Allows attackers to split headers and inject arbitrary HTTP directives!
headers[key] = value;
}
// SECURE: lib/core/request.js patch
function addHeader(headers, key, value) {
// 1. Strict regex checking for dangerous control characters
const INVALID_HEADER_CHAR_REGEX = /[\r\n]/;
if (INVALID_HEADER_CHAR_REGEX.test(key) || INVALID_HEADER_CHAR_REGEX.test(value)) {
throw new TypeError(`Invalid character in header content: ["${key}": "${value}"]`);
}
headers[key] = value;
}