2.1 KiB
nodejs-0001 — CWE-407 in Module._resolveFilename paths dedup
Severity: MEDIUM
File: lib/internal/modules/cjs/loader.js
Symbol: Module._resolveFilename — ArrayPrototypeIncludes(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.js — Module._resolveFilename, the
ArrayIsArray(options.paths) / non-relative branch, lines ~1408–1414.