From 0dc372cc702c622cb5626844c4e4246103a483d5 Mon Sep 17 00:00:00 2001 From: Rohith Pariki Date: Sat, 3 Oct 2026 06:52:27 +0530 Subject: [PATCH] fs: normalize dot segments in rm and rmSync When paths containing '.' or '..' components (e.g., 'a/b/../.') are passed to fs.rm(), fs.rmSync(), or fs.promises.rm(), the different codepaths (native binding.rmSync vs JS rimraf) could produce inconsistent results because the path was used as-is without normalization. Add pathModule.normalize() after getValidatedPath() in all three rm variants to ensure dot-segment components are resolved before the path reaches the underlying removal implementation. Fixes: https://github.com/nodejs/node/issues/61958 Signed-off-by: Rohith Pariki --- lib/fs.js | 5 ++++- lib/internal/fs/promises.js | 1 + test/parallel/test-fs-rm.js | 42 +++++++++++++++++++++++++++++++++++++ 3 files changed, 47 insertions(+), 1 deletion(-) diff --git a/lib/fs.js b/lib/fs.js index 4436fa2df6e6..2ce0a9518e9a 100644 --- a/lib/fs.js +++ b/lib/fs.js @@ -1560,6 +1560,7 @@ function rm(path, options, callback) { if (h !== null && vfsVoid(h.rm(path, options), callback)) return; path = getValidatedPath(path); + path = pathModule.normalize(path); validateRmOptions(path, options, false, (err, options) => { if (err) { @@ -1588,8 +1589,10 @@ function rmSync(path, options) { const result = h.rmSync(path, options); if (result !== undefined) return; } + path = getValidatedPath(path); + path = pathModule.normalize(path); const opts = validateRmOptionsSync(path, options, false); - return binding.rmSync(getValidatedPath(path), opts.maxRetries, opts.recursive, opts.retryDelay); + return binding.rmSync(path, opts.maxRetries, opts.recursive, opts.retryDelay); } /** diff --git a/lib/internal/fs/promises.js b/lib/internal/fs/promises.js index efa981c55e31..18b0816eaf43 100644 --- a/lib/internal/fs/promises.js +++ b/lib/internal/fs/promises.js @@ -1607,6 +1607,7 @@ async function rm(path, options) { if (promise !== undefined) { await promise; return; } } path = getValidatedPath(path); + path = pathModule.normalize(path); options = await validateRmOptionsPromise(path, options, false); return lazyRimRaf()(path, options); } diff --git a/test/parallel/test-fs-rm.js b/test/parallel/test-fs-rm.js index 80600d114b9a..84e2e3aa0dbb 100644 --- a/test/parallel/test-fs-rm.js +++ b/test/parallel/test-fs-rm.js @@ -657,3 +657,45 @@ if (isGitPresent) { // Clean up parent directory fs.rmSync(dirname, { recursive: true, force: true }); } + +// Test that rm/rmSync normalize '.' and '..' in paths before processing. +// Regression test for https://github.com/nodejs/node/issues/61958 +// +// Each variant gets its own base directory to avoid async/sync races. +// The path is constructed by joining components with path.sep so that +// the '..' and '.' are preserved and not pre-normalized by path.join. +{ + // --- rmSync: /a/b/../. should remove /a entirely --- + const base = nextDirPath('dotdot-sync'); + fs.mkdirSync(path.join(base, 'a', 'b', 'c', 'd'), + common.mustNotMutateObjectDeep({ recursive: true })); + const weirdPath = [base, 'a', 'b', '..', '.'].join(path.sep); + fs.rmSync(weirdPath, common.mustNotMutateObjectDeep({ recursive: true })); + assert.strictEqual(fs.existsSync(path.join(base, 'a')), false); +} + +{ + // --- fs.rm (callback): same path construction --- + const base = nextDirPath('dotdot-cb'); + fs.mkdirSync(path.join(base, 'a', 'b', 'c', 'd'), + common.mustNotMutateObjectDeep({ recursive: true })); + const weirdPath = [base, 'a', 'b', '..', '.'].join(path.sep); + fs.rm(weirdPath, + common.mustNotMutateObjectDeep({ recursive: true }), + common.mustSucceed(() => { + assert.strictEqual(fs.existsSync(path.join(base, 'a')), false); + })); +} + +{ + // --- fs.promises.rm: same path construction --- + const base = nextDirPath('dotdot-prom'); + fs.mkdirSync(path.join(base, 'a', 'b', 'c', 'd'), + common.mustNotMutateObjectDeep({ recursive: true })); + const weirdPath = [base, 'a', 'b', '..', '.'].join(path.sep); + fs.promises.rm(weirdPath, + common.mustNotMutateObjectDeep({ recursive: true })) + .then(common.mustCall(() => { + assert.strictEqual(fs.existsSync(path.join(base, 'a')), false); + })); +}