Skip to content

Commit dafefbb

Browse files
committed
path: fix win32 normalize false-positive on reserved names
path.win32.normalize() checked names like CONx or NULs against the Windows reserved device list (CON, NUL, PRN, LPT1, ...) by slicing off the last character whenever the path had no colon, so any file name that happened to be a reserved name plus one extra character got wrongly treated as a device and prefixed with `.\`. A previous attempt at this fix (b0a4f16, reverted in c4429c8) guarded the check with `colonIndex !== -1`, but that also suppressed the colon-less case Windows itself treats as reserved: a name followed immediately by a single dot, e.g. NUL. and COM9. (per Microsoft's own docs, "avoid these names followed immediately by an extension; NUL.txt and NUL.tar.gz are both equivalent to NUL"), which is why it broke Windows CI. This version keeps that dot case working while still rejecting an arbitrary trailing character. Signed-off-by: Alexander Kireev <ak.chatman.media@gmail.com>
1 parent 5d8693e commit dafefbb

2 files changed

Lines changed: 31 additions & 1 deletion

File tree

lib/path.js

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -471,7 +471,20 @@ const win32 = {
471471
} while ((index = StringPrototypeIndexOf(path, ':', index + 1)) !== -1);
472472
}
473473
const colonIndex = StringPrototypeIndexOf(path, ':');
474-
if (isWindowsReservedName(path, colonIndex)) {
474+
if (colonIndex !== -1) {
475+
if (isWindowsReservedName(path, colonIndex)) {
476+
return `.\\${device ?? ''}${tail}`;
477+
}
478+
} else if (
479+
// A reserved name without a colon is only a device path when it is
480+
// followed immediately by a single trailing dot (e.g. `NUL.` and
481+
// `COM9.`, which Windows treats as equivalent to `NUL`/`COM9`). Any
482+
// other trailing character (`CONx`, `NULs`, ...) is a distinct,
483+
// ordinary file name and must be left untouched.
484+
len > 0 &&
485+
StringPrototypeCharCodeAt(path, len - 1) === CHAR_DOT &&
486+
isWindowsReservedName(path, len - 1)
487+
) {
475488
return `.\\${device ?? ''}${tail}`;
476489
}
477490
if (device === undefined) {

test/parallel/test-path-normalize.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,23 @@ assert.strictEqual(path.win32.normalize('foo/bar\\baz'), 'foo\\bar\\baz');
4343
assert.strictEqual(path.win32.normalize('\\\\.\\foo'), '\\\\.\\foo');
4444
assert.strictEqual(path.win32.normalize('\\\\.\\foo\\'), '\\\\.\\foo\\');
4545

46+
// A name that merely starts with a reserved device name is not itself
47+
// reserved: Windows only treats `NAME` as equivalent to the device when it
48+
// is bare, followed by a colon, or followed immediately by a single dot
49+
// (e.g. `NUL.` and `NUL.txt` both refer to the `NUL` device). Any other
50+
// trailing character makes it an ordinary, unrelated file name and it must
51+
// be left untouched.
52+
assert.strictEqual(path.win32.normalize('CONx'), 'CONx');
53+
assert.strictEqual(path.win32.normalize('NULs'), 'NULs');
54+
assert.strictEqual(path.win32.normalize('LPT1x'), 'LPT1x');
55+
assert.strictEqual(path.win32.normalize('PRNzzz'), 'PRNzzz');
56+
assert.strictEqual(path.win32.normalize('CON'), 'CON');
57+
// With a trailing colon or a single trailing dot the reserved-name handling
58+
// still applies.
59+
assert.strictEqual(path.win32.normalize('CON:'), '.\\CON:.');
60+
assert.strictEqual(path.win32.normalize('CON.'), '.\\CON.');
61+
assert.strictEqual(path.win32.normalize('COM9.'), '.\\COM9.');
62+
4663
// Tests related to CVE-2024-36139. Path traversal should not result in changing
4764
// the root directory on Windows.
4865
assert.strictEqual(path.win32.normalize('test/../C:/Windows'), '.\\C:\\Windows');

0 commit comments

Comments
 (0)