diff --git a/lib/path.js b/lib/path.js index 63b037cddfb986..520dc8a574a346 100644 --- a/lib/path.js +++ b/lib/path.js @@ -471,7 +471,20 @@ const win32 = { } while ((index = StringPrototypeIndexOf(path, ':', index + 1)) !== -1); } const colonIndex = StringPrototypeIndexOf(path, ':'); - if (isWindowsReservedName(path, colonIndex)) { + if (colonIndex !== -1) { + if (isWindowsReservedName(path, colonIndex)) { + return `.\\${device ?? ''}${tail}`; + } + } else if ( + // A reserved name without a colon is only a device path when it is + // followed immediately by a single trailing dot (e.g. `NUL.` and + // `COM9.`, which Windows treats as equivalent to `NUL`/`COM9`). Any + // other trailing character (`CONx`, `NULs`, ...) is a distinct, + // ordinary file name and must be left untouched. + len > 0 && + StringPrototypeCharCodeAt(path, len - 1) === CHAR_DOT && + isWindowsReservedName(path, len - 1) + ) { return `.\\${device ?? ''}${tail}`; } if (device === undefined) { diff --git a/test/parallel/test-path-normalize.js b/test/parallel/test-path-normalize.js index 8b537676dbf45d..d704b666e14815 100644 --- a/test/parallel/test-path-normalize.js +++ b/test/parallel/test-path-normalize.js @@ -43,6 +43,23 @@ assert.strictEqual(path.win32.normalize('foo/bar\\baz'), 'foo\\bar\\baz'); assert.strictEqual(path.win32.normalize('\\\\.\\foo'), '\\\\.\\foo'); assert.strictEqual(path.win32.normalize('\\\\.\\foo\\'), '\\\\.\\foo\\'); +// A name that merely starts with a reserved device name is not itself +// reserved: Windows only treats `NAME` as equivalent to the device when it +// is bare, followed by a colon, or followed immediately by a single dot +// (e.g. `NUL.` and `NUL.txt` both refer to the `NUL` device). Any other +// trailing character makes it an ordinary, unrelated file name and it must +// be left untouched. +assert.strictEqual(path.win32.normalize('CONx'), 'CONx'); +assert.strictEqual(path.win32.normalize('NULs'), 'NULs'); +assert.strictEqual(path.win32.normalize('LPT1x'), 'LPT1x'); +assert.strictEqual(path.win32.normalize('PRNzzz'), 'PRNzzz'); +assert.strictEqual(path.win32.normalize('CON'), 'CON'); +// With a trailing colon or a single trailing dot the reserved-name handling +// still applies. +assert.strictEqual(path.win32.normalize('CON:'), '.\\CON:.'); +assert.strictEqual(path.win32.normalize('CON.'), '.\\CON.'); +assert.strictEqual(path.win32.normalize('COM9.'), '.\\COM9.'); + // Tests related to CVE-2024-36139. Path traversal should not result in changing // the root directory on Windows. assert.strictEqual(path.win32.normalize('test/../C:/Windows'), '.\\C:\\Windows');