HTTP Header Injection / TOCTOU
Description
The commit adds a setter for ClientRequest.path that validates assigned values against INVALID_PATH_REGEX, ensuring that path mutations (after construction) cannot inject unsafe characters into HTTP headers. Previously, path could be mutated post-construction without revalidation, potentially allowing CR/LF or other characters to reach _implicitHeader() and cause header injection/TOCTOU issues. The change includes a getter for path, validation on assignment, and regression tests confirming invalid mutations are rejected and valid mutations succeed.
Proof of Concept
POC: Demonstrates pre-fix vulnerability by mutating ClientRequest.path to include CRLF and injecting headers.
Code:
const net = require('net');
const http = require('http');
const server = require('net').createServer((socket) => {
socket.on('data', (chunk) => {
console.log('RAW REQUEST:');
console.log(chunk.toString());
});
});
server.listen(0, '127.0.0.1', () => {
const port = server.address().port;
const req = http.request({ host: '127.0.0.1', port: port, path: '/valid', method: 'GET', createConnection: () => require('net').connect({port, host:'127.0.0.1'}) });
// Mutate after construction to inject CRLF into the request header
req.path = '/evil\r\nX-Injected: true\r\n\r\n';
req.end();
});
Commit Details
Author: Matteo Collina
Date: 2026-03-02 19:10 UTC
Message:
http: validate ClientRequest path on set
The `path` property on `ClientRequest` was only validated at
construction time. Add a getter/setter so that the same
`INVALID_PATH_REGEX` check runs whenever `req.path` is reassigned,
preventing invalid characters from reaching `_implicitHeader()`.
PR-URL: https://github.com/nodejs/node/pull/62030
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Marco Ippolito <marcoippolito54@gmail.com>
Reviewed-By: Tim Perry <pimterry@gmail.com>
Triage Assessment
Vulnerability Type: HTTP Header Injection (via unsafe path mutation) / TOCTOU
Confidence: HIGH
Reasoning:
The commit adds a setter for ClientRequest.path that validates all assigned values against INVALID_PATH_REGEX and throws on unescaped characters. This prevents path mutations from injecting CR/LF or other problematic characters into HTTP headers, addressing a potential header injection/TOCTOU vulnerability when _implicitHeader() is reached. A regression test ensures invalid mutations are rejected and valid mutations succeed.
Verification Assessment
Vulnerability Type: HTTP Header Injection / TOCTOU
Confidence: HIGH
Affected Versions: < 25.9.0
Code Diff
diff --git a/lib/_http_client.js b/lib/_http_client.js
index 1358a441c15f90..c14e899dabbf04 100644
--- a/lib/_http_client.js
+++ b/lib/_http_client.js
@@ -27,6 +27,7 @@ const {
Error,
NumberIsFinite,
ObjectAssign,
+ ObjectDefineProperty,
ObjectKeys,
ObjectSetPrototypeOf,
ReflectApply,
@@ -116,6 +117,7 @@ let debug = require('internal/util/debuglog').debuglog('http', (fn) => {
const INVALID_PATH_REGEX = /[^\u0021-\u00ff]/;
const kError = Symbol('kError');
+const kPath = Symbol('kPath');
const kLenientAll = HTTPParser.kLenientAll | 0;
const kLenientNone = HTTPParser.kLenientNone | 0;
@@ -303,7 +305,7 @@ function ClientRequest(input, options, cb) {
this.joinDuplicateHeaders = options.joinDuplicateHeaders;
- this.path = options.path || '/';
+ this[kPath] = options.path || '/';
if (cb) {
this.once('response', cb);
}
@@ -446,6 +448,22 @@ function ClientRequest(input, options, cb) {
ObjectSetPrototypeOf(ClientRequest.prototype, OutgoingMessage.prototype);
ObjectSetPrototypeOf(ClientRequest, OutgoingMessage);
+ObjectDefineProperty(ClientRequest.prototype, 'path', {
+ __proto__: null,
+ get() {
+ return this[kPath];
+ },
+ set(value) {
+ const path = String(value);
+ if (INVALID_PATH_REGEX.test(path)) {
+ throw new ERR_UNESCAPED_CHARACTERS('Request path');
+ }
+ this[kPath] = path;
+ },
+ configurable: true,
+ enumerable: true,
+});
+
ClientRequest.prototype._finish = function _finish() {
OutgoingMessage.prototype._finish.call(this);
if (hasObserver('http')) {
diff --git a/test/parallel/test-http-client-path-toctou.js b/test/parallel/test-http-client-path-toctou.js
new file mode 100644
index 00000000000000..2975ba363d2614
--- /dev/null
+++ b/test/parallel/test-http-client-path-toctou.js
@@ -0,0 +1,68 @@
+'use strict';
+require('../common');
+const assert = require('assert');
+const http = require('http');
+
+// Test that mutating req.path after construction to include
+// invalid characters (e.g. CRLF) throws ERR_UNESCAPED_CHARACTERS.
+// Regression test for a TOCTOU vulnerability where path was only
+// validated at construction time but could be mutated before
+// _implicitHeader() flushed it to the socket.
+
+// Use a createConnection that returns nothing to avoid actual connection.
+const req = new http.ClientRequest({
+ host: '127.0.0.1',
+ port: 1,
+ path: '/valid',
+ method: 'GET',
+ createConnection: () => {},
+});
+
+// Attempting to set path with CRLF must throw
+assert.throws(
+ () => { req.path = '/evil\r\nX-Injected: true\r\n\r\n'; },
+ {
+ code: 'ERR_UNESCAPED_CHARACTERS',
+ name: 'TypeError',
+ message: 'Request path contains unescaped characters',
+ }
+);
+
+// Path must be unchanged after failed mutation
+assert.strictEqual(req.path, '/valid');
+
+// Attempting to set path with lone CR must throw
+assert.throws(
+ () => { req.path = '/evil\rpath'; },
+ {
+ code: 'ERR_UNESCAPED_CHARACTERS',
+ name: 'TypeError',
+ }
+);
+
+// Attempting to set path with lone LF must throw
+assert.throws(
+ () => { req.path = '/evil\npath'; },
+ {
+ code: 'ERR_UNESCAPED_CHARACTERS',
+ name: 'TypeError',
+ }
+);
+
+// Attempting to set path with null byte must throw
+assert.throws(
+ () => { req.path = '/evil\0path'; },
+ {
+ code: 'ERR_UNESCAPED_CHARACTERS',
+ name: 'TypeError',
+ }
+);
+
+// Valid path mutation should succeed
+req.path = '/also-valid';
+assert.strictEqual(req.path, '/also-valid');
+
+req.path = '/path?query=1&other=2';
+assert.strictEqual(req.path, '/path?query=1&other=2');
+
+req.destroy();