java-topology/defects/nodejs/patch/nodejs-0001-resolve-paths-dedup-set.md

2.1 KiB
Raw Blame History

UNDF: UNDF-2026-000000475

nodejs-0001 — CWE-407 in Module._resolveFilename paths dedup

Severity: MEDIUM File: lib/internal/modules/cjs/loader.js Symbol: Module._resolveFilenameArrayPrototypeIncludes(paths, lookupPaths[j])

Defect

When require.resolve(id, { paths: [...] }) is called with an explicit paths array of length P, the implementation deduplicates lookup paths using a linear scan of the accumulator array:

for (let i = 0; i < options.paths.length; i++) {          // O(P)
  const lookupPaths = Module._resolveLookupPaths(request, fakeParent);  // O(L)
  for (let j = 0; j < lookupPaths.length; j++) {          // O(L)
    if (!ArrayPrototypeIncludes(paths, lookupPaths[j])) { // O(P*L) scan of accumulator
      ArrayPrototypePush(paths, lookupPaths[j]);
    }
  }
}

_resolveLookupPaths returns up to ~20 node_modules ancestor directories per input path. The accumulator paths grows to P×L entries. Each ArrayPrototypeIncludes call scans the entire accumulator: O(P×L) per check, called P×L times total → O(P²×L²) overall.

At P=50 input paths and L=10 lookup paths per entry, the accumulator reaches 500 entries and the dedup loop performs ~250,000 comparisons instead of ~500.

Fix

Replace the accumulator array + linear scan with a Set for O(1) membership:

const pathsSet = new Set();
const paths    = [];

for (let i = 0; i < options.paths.length; i++) {
  const path = options.paths[i];
  fakeParent.paths = Module._nodeModulePaths(path);
  const lookupPaths = Module._resolveLookupPaths(request, fakeParent);

  for (let j = 0; j < lookupPaths.length; j++) {
    if (!pathsSet.has(lookupPaths[j])) {          // O(1)
      pathsSet.add(lookupPaths[j]);
      ArrayPrototypePush(paths, lookupPaths[j]);
    }
  }
}

Complexity

Scenario Before After
P=50 paths, L=10 lookup paths each O(P²×L²) = ~250k ops O(P×L) = ~500 ops
Ratio at P=50, L=10 500× 1×

Source location

lib/internal/modules/cjs/loader.jsModule._resolveFilename, the ArrayIsArray(options.paths) / non-relative branch, lines ~14081414.