Oct 31, 2026·10 min read·3 visits
Missing return statements in node-glob's AbortSignal handling logic cause directory traversal operations to continue running in the background even after an operation is aborted or times out. Upgrading to glob version 14.0.2 resolves the issue by properly terminating the control flow upon signal abortion.
CVE-2026-118773 (GHSA-3g3m-2fjr-6pw4) is an uncontrolled resource consumption vulnerability in the node-glob npm package affecting versions 9.0.0 through 14.0.1. Due to missing return statements in the AbortSignal check routines within the directory traversal walker, aborted operations reject the caller promise as expected but continue executing background filesystem reads and pattern matching. Applications that rely on AbortSignal or AbortSignal.timeout() to enforce execution budgets on user-influenced glob operations remain susceptible to CPU, event loop, and disk I/O exhaustion.
The glob package (node-glob) is a foundational filesystem pattern-matching library within the Node.js ecosystem. It provides synchronous, asynchronous, and stream-based interfaces for traversing directory trees and resolving file paths against wildcard expressions. Beginning with version 9.0.0, node-glob introduced a refactored architecture built around the GlobUtil traversal engine in src/walker.ts, which supports cooperative cancellation via the standard JavaScript AbortSignal interface.
In versions 9.0.0 through 14.0.1, node-glob contains an asynchronous control flow flaw classified under CWE-400 (Uncontrolled Resource Consumption). When a caller passes an AbortSignal (such as one generated via AbortController or AbortSignal.timeout()) into a glob operation, the traversal engine checks the aborted property of the signal before initiating or continuing directory walks. However, the abort guard clauses invoke the completion callback without returning from the enclosing traversal methods.
Because the guard clauses do not terminate execution of the function, control flow falls through to the subsequent directory reading and pattern-processing routines. While the top-level Promise returned to the caller immediately rejects with an AbortError (or the stream emits an error event), the internal worker continues traversing the target directory hierarchy in the background.
This discrepancy between caller notification and actual background execution creates an attack surface for applications that expose file search functionality or process untrusted path patterns. If a service relies on AbortSignal timeouts to bound the cost of expensive recursive searches (such as **/*), an attacker can trigger repeated searches that time out at the application layer while stacking un-cancellable filesystem operations on the backend.
The root cause of CVE-2026-118773 resides in the GlobUtil abstract class within src/walker.ts. GlobUtil implements both asynchronous and synchronous callback-based traversal pipelines through four core methods: walkCB, walkCB2, walkCBSync, and walkCB2Sync. These methods are responsible for coordinating interaction with the PathScurry filesystem abstraction and the internal Processor pattern-matching state machine.
At the entry point of each of these four methods, the developer implemented an abortion check intended to short-circuit traversal if this.signal?.aborted evaluated to true. The check was written as if (this.signal?.aborted) cb(). In callback-driven JavaScript architectures, invoking the callback cb() signals completion to the caller, but it does not automatically exit the current stack frame unless paired with an explicit return statement.
In walkCB and walkCBSync, immediately after invoking cb(), execution proceeded directly to this.walkCB2(target, patterns, new Processor(this.opts), cb) (or walkCB2Sync). Within walkCB2 and walkCB2Sync, after invoking cb() on an aborted signal, execution continued to evaluate pause states (if (this.paused)) and subsequently executed processor.processPatterns(target, patterns). This method queues further directory reads (readdirCB / readdirSync) and recursively spawns additional walkCB2 invocations for every matched child directory.
Additionally, the onResume method contained an early return check (if (this.signal?.aborted) return). When a walk was paused and then aborted, callbacks queued via onResume were silently dropped without invoking their completion handlers, while unpaused walks ignored the abort signal's control flow boundary entirely.
The vulnerability was remediated in commit 4682e8c9babee1b385202ede36389634e12076d7. The patch modifies src/walker.ts across all four traversal methods in GlobUtil to ensure that invoking the completion callback on an aborted signal immediately exits the function.
// Vulnerable implementation in src/walker.ts (v9.0.0 - v14.0.1)
walkCB(target: Path, patterns: Pattern[], cb: () => unknown) {
if (this.signal?.aborted) cb() // BUG: Missing return statement
this.walkCB2(target, patterns, new Processor(this.opts), cb)
}
walkCB2(
target: Path,
patterns: Pattern[],
processor: Processor,
cb: () => unknown,
) {
if (this.#childrenIgnored(target)) return cb()
if (this.signal?.aborted) cb() // BUG: Missing return statement
if (this.paused) {
this.onResume(() => this.walkCB2(target, patterns, processor, cb))
return
}
// Execution falls through to pattern processing and directory reads
processor.processPatterns(target, patterns)
// ...
}Notice how the line if (this.#childrenIgnored(target)) return cb() immediately preceding the signal check correctly utilized return cb(), whereas the subsequent if (this.signal?.aborted) cb() omitted the return keyword. The exact same pattern was duplicated in the synchronous equivalents walkCBSync and walkCB2Sync.
--- a/src/walker.ts
+++ b/src/walker.ts
@@ -155,7 +155,6 @@ export abstract class GlobUtil<O extends GlobWalkerOpts = GlobWalkerOpts> {
}
}
onResume(fn: () => unknown) {
- if (this.signal?.aborted) return
/* c8 ignore start */
if (!this.paused) {
fn()
@@ -281,7 +280,7 @@ export abstract class GlobUtil<O extends GlobWalkerOpts = GlobWalkerOpts> {
walkCB(target: Path, patterns: Pattern[], cb: () => unknown) {
/* c8 ignore start */
- if (this.signal?.aborted) cb()
+ if (this.signal?.aborted) return cb()
/* c8 ignore stop */
this.walkCB2(target, patterns, new Processor(this.opts), cb)
}
@@ -293,7 +292,7 @@ export abstract class GlobUtil<O extends GlobWalkerOpts = GlobWalkerOpts> {
cb: () => unknown,
) {
if (this.#childrenIgnored(target)) return cb()
- if (this.signal?.aborted) cb()
+ if (this.signal?.aborted) return cb()
if (this.paused) {
this.onResume(() => this.walkCB2(target, patterns, processor, cb))
return
@@ -365,7 +364,7 @@ export abstract class GlobUtil<O extends GlobWalkerOpts = GlobWalkerOpts> {
walkCBSync(target: Path, patterns: Pattern[], cb: () => unknown) {
/* c8 ignore start */
- if (this.signal?.aborted) cb()
+ if (this.signal?.aborted) return cb()
/* c8 ignore stop */
this.walkCB2Sync(target, patterns, new Processor(this.opts), cb)
}
@@ -377,7 +376,7 @@ export abstract class GlobUtil<O extends GlobWalkerOpts = GlobWalkerOpts> {
cb: () => unknown,
) {
if (this.#childrenIgnored(target)) return cb()
- if (this.signal?.aborted) cb()
+ if (this.signal?.aborted) return cb()
if (this.paused) {
this.onResume(() =>
this.walkCB2Sync(target, patterns, processor, cb),The patch is complete and directly addresses the control flow bypass across both asynchronous and synchronous execution paths. By removing if (this.signal?.aborted) return from onResume and enforcing return cb() inside walkCB, walkCB2, walkCBSync, and walkCB2Sync, any pending or resumed walk steps cleanly invoke their completion callbacks and halt further recursion as soon as the signal state transitions to aborted.
Exploiting CVE-2026-118773 requires an application to invoke glob over a non-trivial directory tree while relying on an AbortSignal to enforce cancellation or execution timeouts. This pattern commonly occurs in web servers, IDE language servers, build tools, or CI/CD runners that expose search endpoints or process user-supplied file globs with a timeout guard (e.g., AbortSignal.timeout(100)).
In an attack scenario, an unauthenticated or low-privileged user submits a request containing a broad recursive glob pattern such as **/* or deeply nested brace expansions like **/{a,b,c,d}/**. After the configured timeout interval elapses, the AbortSignal fires. The application catches the rejected Promise from glob() and immediately returns an error response (such as HTTP 408 or 504) to the client, freeing the client to send another request.
Meanwhile, the Node.js process continues executing walkCB2 and fs.readdir across every subdirectory in the target scope. Because the caller's Promise has already settled, the application has no visibility into or control over the orphaned traversal tasks. By issuing concurrent requests that rapidly time out, an attacker multiplies the number of active filesystem walks, saturating the libuv threadpool with readdir system calls and blocking the main JavaScript event loop with pattern-matching calculations.
The regression test included in commit 4682e8c9babee1b385202ede36389634e12076d7 demonstrates the mechanics of the bug by wrapping fs.readdir and fs.readdirSync to count filesystem invocations after an AbortController triggers on the very first read:
t.test('no more reads after abort', async t => {
const mk = (n: number): Record<string, any> =>
n ? { a: mk(n - 1), b: mk(n - 1), f: '' } : { f: '' }
const cwd = t.testdir(mk(5))
const counted = (ac: AbortController) => {
const c = { calls: 0 }
const abortFirst = () => {
if (++c.calls === 1) ac.abort(yeet)
}
const cfs = {
...fs,
readdir: (...args: any[]) => {
abortFirst()
return (fs.readdir as any)(...args)
},
readdirSync: (...args: any[]) => {
abortFirst()
return (fs.readdirSync as any)(...args)
},
}
return { c, cfs }
}
t.test('async', async t => {
const ac = new AbortController()
const { c, cfs } = counted(ac)
await t.rejects(glob('**', { cwd, signal: ac.signal, fs: cfs }), yeet)
const atReject = c.calls
// On vulnerable versions (< 14.0.2), c.calls continues to increment
// during the 100ms delay as background walks traverse the tree.
await new Promise(r => setTimeout(r, 100))
t.equal(c.calls, atReject)
})
})The security impact of CVE-2026-118773 is confined to system availability, with a CVSS v4.0 base score of 6.3 (Medium) (CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:N/VA:L/SC:N/SI:N/SA:N). The vulnerability does not allow unauthorized disclosure of file contents (VC:N) because the results of the orphaned directory walks are discarded after the outer Promise rejects, nor does it permit modification of the filesystem (VI:N).
The AT:P (Attack Requirements: Present) metric reflects that the vulnerability only manifests in environments where AbortSignal is actively used as a control mechanism to bound glob execution. Applications that do not pass a signal option to glob are unaffected by this specific flaw, though they would already lack timeout protection against expensive traversals.
In affected deployments, the availability impact (VA:L) stems from the exhaustion of three shared resources in a Node.js runtime: CPU time on the main V8 event loop thread (spent evaluating regular expressions and Minimatch patterns), libuv worker threads (occupied by asynchronous uv_fs_scandir operations), and operating system file descriptors and disk I/O bandwidth.
According to CISA SSVC (Stakeholder-Specific Vulnerability Categorization) metrics, exploitation is currently rated as none, the vulnerability is marked as not automatable (no), and technical impact is classified as partial. The EPSS score of 0.0035 (0.35%, 28.69th percentile) indicates a low probability of widespread opportunistic exploitation in the wild.
The primary remediation for CVE-2026-118773 is to upgrade the glob dependency to version 14.0.2 or later. Version 14.0.2 incorporates commit 4682e8c9babee1b385202ede36389634e12076d7, which restores proper control flow termination when an AbortSignal is triggered.
# Upgrade using npm
npm install glob@^14.0.2
# Upgrade using yarn
yarn add glob@^14.0.2
# Upgrade using pnpm
pnpm update glob> [!NOTE]
> Because node-glob v14 requires modern Node.js runtime environments, projects pinned to older major versions of glob (such as v9, v10, v11, v12, or v13) that cannot immediately upgrade to v14.0.2 must implement application-layer mitigations to prevent resource exhaustion.
For environments where upgrading to 14.0.2 is blocked by breaking API changes or runtime constraints, security engineers should apply defense-in-depth controls around filesystem operations:
glob(). Specifically, disallow arbitrary recursive globstar (**) patterns or deeply nested brace expansions from untrusted callers, or combine them with strict maxDepth options.cwd option is restricted to narrow, dedicated subdirectories with bounded file counts rather than broad application or system root directories, and enable dot: false (default) alongside explicit ignore rules (e.g., **/node_modules/**, **/.git/**) so this.#childrenIgnored(target) halts traversal early.AbortSignal cancellation for untrusted workloads on vulnerable versions. Execute filesystem traversals inside isolated worker_threads or child processes and enforce timeouts by calling worker.terminate() or sending SIGKILL to the child process when the execution budget expires.CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:N/VA:L/SC:N/SI:N/SA:N| Product | Affected Versions | Fixed Version |
|---|---|---|
glob isaacs | >= 9.0.0, < 14.0.2 | 14.0.2 |
| Attribute | Detail |
|---|---|
| Vulnerability ID | CVE-2026-118773 / GHSA-3g3m-2fjr-6pw4 |
| Weakness Class | CWE-400 (Uncontrolled Resource Consumption) |
| Affected Component | glob (node-glob) >= 9.0.0, < 14.0.2 |
| CVSS v4.0 Score | 6.3 Medium (CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:N/VA:L/SC:N/SI:N/SA:N) |
| EPSS Score | 0.35% (28.69th Percentile) |
| Impact | Post-timeout background filesystem traversal leading to I/O and CPU exhaustion |
| Exploit Status | Proof-of-Concept (Unit test reproduction in patch commit) |
| CISA KEV Status | Not Listed |
The software does not properly restrict the allocation or consumption of limited system resources (such as I/O operations, file handles, or CPU time) when an operation should have ceased.
Authlib versions 0.15 through 1.9.0 contain an authorization bypass vulnerability (CWE-639) in the RFC 7592 ClientConfigurationEndpoint implementation and its corresponding reference documentation. The framework fails to verify that the presented registration access token belongs to the target client specified in the request, and the official reference implementations for authenticate_client and check_permission omit token-to-client binding checks. Consequently, an authenticated client holding any valid registration access token can read, modify, or delete the registration metadata of arbitrary clients on the authorization server.
CVE-2026-118764 (also tracked as GHSA-g3hv-75wg-m249) is an asymmetric resource consumption vulnerability (CWE-405) in the npm package minimatch prior to version 11.0.1. When pattern compilation is invoked with an optimizationLevel of 2 or higher, the internal preprocessing functions firstPhasePreProcess() and secondPhasePreProcess() in src/index.ts perform unbounded pattern splitting and quadratic pairwise deduplication. An attacker supplying a crafted glob string containing repeated **/.. segments can induce a combinatorial explosion of pattern permutations followed by an O(M^2) comparison routine, blocking the Node.js event loop and exhausting process memory.
A regular expression denial of service (ReDoS) vulnerability exists in @hapi/content from version 3.0.6 prior to 6.0.3. The Content.disposition() function utilizes an unanchored parameter-name regular expression inside a global replacement scan. When processing a Content-Disposition header parameter containing a long token lacking an equals sign, the JavaScript regular expression engine undergoes quadratic O(N^2) backtracking. Because @hapi/content is invoked by @hapi/pez during multipart/form-data request parsing, unauthenticated remote attackers can send a crafted multipart request to any Hapi route with multipart payload parsing enabled, starving the single-threaded Node.js event loop.
A Regular Expression Denial of Service (ReDoS) vulnerability exists in the joi schema description and data validation library for JavaScript. When validating multiple email addresses using the Joi.string().email({ multiple: true }) configuration, the library compiles a separator regular expression with an unanchored greedy leading whitespace quantifier. Supplying an input string containing a long sequence of whitespace characters without the expected delimiter triggers quadratic time complexity during String.prototype.split execution, blocking the single-threaded Node.js event loop.
CVE-2026-119205 (GHSA-mfr5-77xh-9j7h) is a Denial of Service (DoS) vulnerability in pypdf prior to version 6.21.4. The flaw occurs during the cloning or duplication of nested PDF dictionary structures. Because the cycle-detection algorithm within DictionaryObject._clone() tracked generated destination object references rather than the original source indirect references, cyclic reference graphs in source PDFs evaded detection, resulting in an infinite loop and CPU exhaustion.
CVE-2026-119204 (also tracked as GHSA-cggf-277g-3q54) is a Denial of Service (DoS) vulnerability in pypdf prior to version 6.21.4. The vulnerability stems from uncontrolled resource consumption (CWE-400) during the flattening of a PDF document's page tree. By supplying a compact PDF containing a hierarchical amplification structure of shared `/Pages` and `/Page` nodes with dense dictionary entries or inherited resources, an unauthenticated attacker can cause pypdf to copy millions of dictionary entries into individual `PageObject` instances without enforcing a document-wide entry budget. This triggers severe heap memory exhaustion and process termination via Out-Of-Memory (OOM) kills.