# UNDF: UNDF-2026-000000475 # 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: ```js 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: ```js 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.