URL handling / path construction leading to potential misrouting or path traversal

MEDIUM
nodejs/node
Commit: eb92eac63c59
Affected: undici <= 7.24.3 (as vendored by Node.js 25.9.0)
2026-04-05 10:14 UTC

Description

The commit includes a real fix to how undici constructs the HTTP request path used when dispatching a fetch in the HTTP client. Previously, the path was derived by slicing url.href, which could drop an explicit trailing '?' that denotes an empty query string. The patch reconstructs the path as url.pathname + url.search and adds a guard to preserve a trailing '?' when present. This hardens URL/path handling in the HTTP client, reducing edge-case discrepancies that could lead to misrouting or injection-like issues in some servers or proxies that distinguish /path from /path? (empty query). Although this is implemented as a dependency update to undici, it changes core URL handling logic and fixes a vulnerability surface related to URL construction in fetch paths.

Proof of Concept

PoC idea to demonstrate the effect of preserving trailing '?' in the path: 1) Run a small HTTP server that logs the exact request URL (req.url). 2) Use undici (or a compatible fetch) to request the server with a URL that includes a trailing '?' and one without: - http://localhost:3000/test? - http://localhost:3000/test 3) Observe what the server logs for the two requests. With the vulnerable pre-fix, the client could drop the trailing '?' and the server would see '/test' for both cases, potentially changing how the request is routed or how server-side logic interprets an empty query. With the fix in 7.24.4, the request with a trailing '?' should be observed as '/test?' on servers that preserve the exact URL, ensuring correct semantics. Node server (example): const http = require('http'); const port = 3000; const server = http.createServer((req, res) => { console.log('Received:', req.method, req.url); res.end('OK'); }); server.listen(port, () => console.log('Server listening on', port)); Client (using undici): const { fetch } = require('undici'); async function test(url){ const res = await fetch(url); await res.text(); console.log('Fetched', url); } (async () => { await test('http://localhost:3000/test?'); await test('http://localhost:3000/test'); })(); Expected if the fix is effective: the server should observe '/test?' for the first request and '/test' (or '/test') for the second; depending on server/proxy behavior, this preserves the explicit empty query distinction that some environments rely on. This PoC demonstrates how the trailing '?' is semantically significant and how the fix ensures it is conveyed to the server.

Commit Details

Author: Node.js GitHub Bot

Date: 2026-03-16 21:42 UTC

Message:

deps: update undici to 7.24.4 PR-URL: https://github.com/nodejs/node/pull/62271 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com> Reviewed-By: Colin Ihrig <cjihrig@gmail.com>

Triage Assessment

Vulnerability Type: Path Traversal / URL handling

Confidence: MEDIUM

Reasoning:

The commit updates the undici dependency to 7.24.4 and adjusts how the request path is constructed when dispatching, including handling for trailing '?' in the URL. This tightens URL/path handling in the HTTP fetch path, which can reduce potential path construction issues that could lead to misrouting or injection-like problems in edge cases. While it's a dependency patch, the change appears aligned with hardening URL/path handling in the HTTP client, which is a common vector for security improvements.

Verification Assessment

Vulnerability Type: URL handling / path construction leading to potential misrouting or path traversal

Confidence: MEDIUM

Affected Versions: undici <= 7.24.3 (as vendored by Node.js 25.9.0)

Code Diff

diff --git a/deps/undici/src/lib/llhttp/wasm_build_env.txt b/deps/undici/src/lib/llhttp/wasm_build_env.txt index dbad8c4d5550a2..bcc1c79374ce5c 100644 --- a/deps/undici/src/lib/llhttp/wasm_build_env.txt +++ b/deps/undici/src/lib/llhttp/wasm_build_env.txt @@ -1,5 +1,5 @@ -> undici@7.24.3 build:wasm +> undici@7.24.4 build:wasm > node build/wasm.js --docker > docker run --rm --platform=linux/x86_64 --user 1001:1001 --mount type=bind,source=/home/runner/work/node/node/deps/undici/src/lib/llhttp,target=/home/node/build/lib/llhttp --mount type=bind,source=/home/runner/work/node/node/deps/undici/src/build,target=/home/node/build/build --mount type=bind,source=/home/runner/work/node/node/deps/undici/src/deps,target=/home/node/build/deps -t ghcr.io/nodejs/wasm-builder@sha256:975f391d907e42a75b8c72eb77c782181e941608687d4d8694c3e9df415a0970 node build/wasm.js diff --git a/deps/undici/src/lib/web/fetch/index.js b/deps/undici/src/lib/web/fetch/index.js index a89b103fa4577c..8b88904e9d53ff 100644 --- a/deps/undici/src/lib/web/fetch/index.js +++ b/deps/undici/src/lib/web/fetch/index.js @@ -2132,9 +2132,12 @@ async function httpNetworkFetch ( /** @type {import('../../..').Agent} */ const agent = fetchParams.controller.dispatcher + const path = url.pathname + url.search + const hasTrailingQuestionMark = url.search.length === 0 && url.href[url.href.length - url.hash.length - 1] === '?' + return new Promise((resolve, reject) => agent.dispatch( { - path: url.href.slice(url.href.indexOf(url.host) + url.host.length, url.hash.length ? -url.hash.length : undefined), + path: hasTrailingQuestionMark ? `${path}?` : path, origin: url.origin, method: request.method, body: agent.isMockActive ? request.body && (request.body.source || request.body.stream) : body, diff --git a/deps/undici/src/package.json b/deps/undici/src/package.json index 1f1c50df32bf2c..d27058bcc39ea0 100644 --- a/deps/undici/src/package.json +++ b/deps/undici/src/package.json @@ -1,6 +1,6 @@ { "name": "undici", - "version": "7.24.3", + "version": "7.24.4", "description": "An HTTP/1.1 client, written from scratch for Node.js", "homepage": "https://undici.nodejs.org", "bugs": { diff --git a/deps/undici/undici.js b/deps/undici/undici.js index 4cc53a03781f61..ee62cc69315bab 100644 --- a/deps/undici/undici.js +++ b/deps/undici/undici.js @@ -13611,9 +13611,11 @@ var require_fetch = __commonJS({ function dispatch({ body }) { const url = requestCurrentURL(request); const agent = fetchParams.controller.dispatcher; + const path = url.pathname + url.search; + const hasTrailingQuestionMark = url.search.length === 0 && url.href[url.href.length - url.hash.length - 1] === "?"; return new Promise((resolve, reject) => agent.dispatch( { - path: url.href.slice(url.href.indexOf(url.host) + url.host.length, url.hash.length ? -url.hash.length : void 0), + path: hasTrailingQuestionMark ? `${path}?` : path, origin: url.origin, method: request.method, body: agent.isMockActive ? request.body && (request.body.source || request.body.stream) : body, diff --git a/src/undici_version.h b/src/undici_version.h index 740c26eb34317d..968e0b8b8b6a88 100644 --- a/src/undici_version.h +++ b/src/undici_version.h @@ -2,5 +2,5 @@ // Refer to tools/dep_updaters/update-undici.sh #ifndef SRC_UNDICI_VERSION_H_ #define SRC_UNDICI_VERSION_H_ -#define UNDICI_VERSION "7.24.3" +#define UNDICI_VERSION "7.24.4" #endif // SRC_UNDICI_VERSION_H_
← Back to Alerts View on GitHub →