Denial of Service (infinite authentication-retry loop / DoS via repeated requests)

MEDIUM
nodejs/node
Commit: 8117cb03f49e
Affected: Node.js 25.9.0 and earlier (bundled undici 7.19.0)
2026-04-05 11:08 UTC

Description

The commit includes a real code fix in the undici fetch path: when handling a 401 authentication scenario in browsers, the fetch logic now returns a network error instead of retrying, to prevent an infinite loop of re-sending requests after browser prompts for credentials. This mitigates a potential Denial of Service due to looping retries, and is accompanied by a dependency bump from undici 7.19.0 to 7.19.1. While the core change is a robustness guard, it targets a security-sensitive control flow and reduces the risk of resource exhaustion in adversarial or misbehaving environments.

Proof of Concept

PoC (conceptual and minimal): Goal: Demonstrate how an infinite retry loop could occur when a client re-sends after a 401 challenge is presented in a browser, and how the fix prevents it by returning a network error instead of continuing retries. 1) Start a local HTTP server that always challenges with 401 on a protected resource: // server.js const http = require('http'); const port = 3000; let firstRequest = true; const server = http.createServer((req, res) => { if (req.url === '/protected') { res.statusCode = 401; res.setHeader('WWW-Authenticate', 'Basic realm="example"'); res.end('Unauthorized'); } else { res.end('OK'); } }); server.listen(port, () => console.log(`server listening on http://localhost:${port}`)); 2) Client request using undici fetch to trigger negotiation flow: // client.js const { fetch } = require('undici'); (async () => { try { const res = await fetch('http://localhost:3000/protected'); console.log('Response status:', res.status); const text = await res.text(); console.log('Response body:', text); } catch (err) { console.error('Request failed:', err); } })(); 3) Expected behavior (pre-fix): Depending on environment (e.g., a browser), after a 401 and a user prompt for credentials, the client might automatically retry the request. If the server continues to respond 401, this could result in an infinite loop of retries and network resource exhaustion. 4) Expected behavior (post-fix): With the fix, when such a 401 scenario that would trigger retries in the browser occurs, the client will return a network error instead of re-sending indefinitely, preventing a potential DoS-like loop. Notes: - The PoC requires running the server and the undici-based client locally. In a real browser, the user prompt for credentials and the browser's retry behavior may influence observed behavior. The essential point is that the updated code path guards against an unbounded retry loop by emitting a network error instead of looping.

Commit Details

Author: Node.js GitHub Bot

Date: 2026-01-25 17:29 UTC

Message:

deps: update undici to 7.19.1 PR-URL: https://github.com/nodejs/node/pull/61514 Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com> Reviewed-By: Richard Lau <richard.lau@ibm.com> Reviewed-By: Colin Ihrig <cjihrig@gmail.com>

Triage Assessment

Vulnerability Type: Denial of Service

Confidence: MEDIUM

Reasoning:

The diff includes a code change in undici fetch logic that returns a network error to prevent an infinite 401 loop when re-sending requests after authentication prompts in browsers. This is a security-related robustness fix (mitigating a potential denial-of-service-like loop) accompanying a dependency bump to undici 7.19.1. While the primary commit is a dependency update, the in-code change targets a security-sensitive control flow.

Verification Assessment

Vulnerability Type: Denial of Service (infinite authentication-retry loop / DoS via repeated requests)

Confidence: MEDIUM

Affected Versions: Node.js 25.9.0 and earlier (bundled undici 7.19.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 e0837fc906bc30..445df079257810 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.19.0 build:wasm +> undici@7.19.1 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 56e540d9d88258..24d724c2218898 100644 --- a/deps/undici/src/lib/web/fetch/index.js +++ b/deps/undici/src/lib/web/fetch/index.js @@ -1675,6 +1675,11 @@ async function httpNetworkOrCacheFetch ( // 4. Set the password given request’s current URL and password. // requestCurrentURL(request).password = TODO + + // In browsers, the user will be prompted to enter a username/password before the request + // is re-sent. To prevent an infinite 401 loop, return a network error for now. + // https://github.com/nodejs/undici/pull/4756 + return makeNetworkError() } // 4. Set response to the result of running HTTP-network-or-cache fetch given diff --git a/deps/undici/src/package.json b/deps/undici/src/package.json index 18637c469dedae..95a13d54f6fce8 100644 --- a/deps/undici/src/package.json +++ b/deps/undici/src/package.json @@ -1,6 +1,6 @@ { "name": "undici", - "version": "7.19.0", + "version": "7.19.1", "description": "An HTTP/1.1 client, written from scratch for Node.js", "homepage": "https://undici.nodejs.org", "bugs": { diff --git a/deps/undici/src/scripts/release.js b/deps/undici/src/scripts/release.js index dd3e86eb5dc760..7ca72d8dd3dd04 100644 --- a/deps/undici/src/scripts/release.js +++ b/deps/undici/src/scripts/release.js @@ -2,7 +2,7 @@ // Called from .github/workflows -const generateReleaseNotes = async ({ github, owner, repo, versionTag, defaultBranch }) => { +const generateReleaseNotes = async ({ github, owner, repo, versionTag, commitHash }) => { const { data: releases } = await github.rest.repos.listReleases({ owner, repo @@ -14,7 +14,7 @@ const generateReleaseNotes = async ({ github, owner, repo, versionTag, defaultBr owner, repo, tag_name: versionTag, - target_commitish: defaultBranch, + target_commitish: commitHash, previous_tag_name: previousRelease?.tag_name }) @@ -25,9 +25,9 @@ const generateReleaseNotes = async ({ github, owner, repo, versionTag, defaultBr return bodyWithoutReleasePr } -const generatePr = async ({ github, context, defaultBranch, versionTag }) => { +const generatePr = async ({ github, context, defaultBranch, versionTag, commitHash }) => { const { owner, repo } = context.repo - const releaseNotes = await generateReleaseNotes({ github, owner, repo, versionTag, defaultBranch }) + const releaseNotes = await generateReleaseNotes({ github, owner, repo, versionTag, commitHash }) await github.rest.pulls.create({ owner, @@ -39,15 +39,15 @@ const generatePr = async ({ github, context, defaultBranch, versionTag }) => { }) } -const release = async ({ github, context, defaultBranch, versionTag }) => { +const release = async ({ github, context, versionTag, commitHash }) => { const { owner, repo } = context.repo - const releaseNotes = await generateReleaseNotes({ github, owner, repo, versionTag, defaultBranch }) + const releaseNotes = await generateReleaseNotes({ github, owner, repo, versionTag, commitHash }) await github.rest.repos.createRelease({ owner, repo, tag_name: versionTag, - target_commitish: defaultBranch, + target_commitish: commitHash, name: versionTag, body: releaseNotes, draft: false, diff --git a/deps/undici/undici.js b/deps/undici/undici.js index 77efb43720baec..f09f633f969ffc 100644 --- a/deps/undici/undici.js +++ b/deps/undici/undici.js @@ -12595,6 +12595,7 @@ var require_fetch = __commonJS({ if (isCancelled(fetchParams)) { return makeAppropriateNetworkError(fetchParams); } + return makeNetworkError(); } fetchParams.controller.connection.destroy(); response = await httpNetworkOrCacheFetch(fetchParams, true); diff --git a/src/undici_version.h b/src/undici_version.h index 3b69219c7e4865..0bbb7571b3ed9c 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.19.0" +#define UNDICI_VERSION "7.19.1" #endif // SRC_UNDICI_VERSION_H_
← Back to Alerts View on GitHub →