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);
+ }));
+}