Skip to content

Commit 0b631bb

Browse files
RafaelGSSrichardlau
authored andcommitted
lib: handle windows reserved device names on UNC
We have found that UNC paths weren't covered when .join/.normalize windows reserved device names (COM1, LPT1). PR-URL: #59286 Backport-PR-URL: #59831 Reviewed-By: Robert Nagy <[email protected]> Reviewed-By: Matteo Collina <[email protected]> Reviewed-By: James M Snell <[email protected]>
1 parent 9cc89f5 commit 0b631bb

File tree

3 files changed

+60
-0
lines changed

3 files changed

+60
-0
lines changed

lib/path.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,13 @@ const win32 = {
415415
// We matched a device root (e.g. \\\\.\\PHYSICALDRIVE0)
416416
device = `\\\\${firstPart}`;
417417
rootEnd = 4;
418+
const colonIndex = StringPrototypeIndexOf(path, ':');
419+
// Special case: handle \\?\COM1: or similar reserved device paths
420+
const possibleDevice = StringPrototypeSlice(path, 4, colonIndex + 1);
421+
if (isWindowsReservedName(possibleDevice, possibleDevice.length - 1)) {
422+
device = `\\\\?\\${possibleDevice}`;
423+
rootEnd = 4 + possibleDevice.length;
424+
}
418425
} else if (j === len) {
419426
// We matched a UNC root only
420427
// Return the normalized version of the UNC root since there
@@ -573,6 +580,36 @@ const win32 = {
573580
joined = `\\${StringPrototypeSlice(joined, slashCount)}`;
574581
}
575582

583+
// Skip normalization when reserved device names are present
584+
const parts = [];
585+
let part = '';
586+
587+
for (let i = 0; i < joined.length; i++) {
588+
if (joined[i] === '\\') {
589+
if (part) parts.push(part);
590+
part = '';
591+
// Skip consecutive backslashes
592+
while (i + 1 < joined.length && joined[i + 1] === '\\') i++;
593+
} else {
594+
part += joined[i];
595+
}
596+
}
597+
// Add the final part if any
598+
if (part) parts.push(part);
599+
600+
// Check if any part has a Windows reserved name
601+
if (parts.some((p) => {
602+
const colonIndex = StringPrototypeIndexOf(p, ':');
603+
return colonIndex !== -1 && isWindowsReservedName(p, colonIndex);
604+
})) {
605+
// Replace forward slashes with backslashes
606+
let result = '';
607+
for (let i = 0; i < joined.length; i++) {
608+
result += joined[i] === '/' ? '\\' : joined[i];
609+
}
610+
return result;
611+
}
612+
576613
return win32.normalize(joined);
577614
},
578615

test/parallel/test-path-join.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,14 @@ joinTests.push([
110110
[['c:.', 'file'], 'c:file'],
111111
[['c:', '/'], 'c:\\'],
112112
[['c:', 'file'], 'c:\\file'],
113+
// UNC path join tests (Windows)
114+
[['\\server\\share', 'file.txt'], '\\server\\share\\file.txt'],
115+
[['\\server\\share', 'folder', 'another.txt'], '\\server\\share\\folder\\another.txt'],
116+
[['\\server\\share', 'COM1:'], '\\server\\share\\COM1:'],
117+
[['\\server\\share', 'path', 'LPT1:'], '\\server\\share\\path\\LPT1:'],
118+
[['\\fileserver\\public\\uploads', 'CON:..\\..\\..\\private\\db.conf'],
119+
'\\fileserver\\public\\uploads\\CON:..\\..\\..\\private\\db.conf'],
120+
113121
// Path traversal in previous versions of Node.js.
114122
[['./upload', '/../C:/Windows'], '.\\C:\\Windows'],
115123
[['upload', '../', 'C:foo'], '.\\C:foo'],

test/parallel/test-path-win32-normalize-device-names.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,19 @@ if (!common.isWindows) {
99
}
1010

1111
const normalizeDeviceNameTests = [
12+
// UNC paths: \\server\share\... is a Windows UNC path, where 'server' is the network server name and 'share'
13+
// is the shared folder. These are used for network file access and are subject to reserved device name
14+
// checks after the share.
15+
{ input: '\\\\server\\share\\COM1:', expected: '\\\\server\\share\\COM1:' },
16+
{ input: '\\\\server\\share\\PRN:', expected: '\\\\server\\share\\PRN:' },
17+
{ input: '\\\\server\\share\\AUX:', expected: '\\\\server\\share\\AUX:' },
18+
{ input: '\\\\server\\share\\LPT1:', expected: '\\\\server\\share\\LPT1:' },
19+
{ input: '\\\\server\\share\\COM1:\\foo\\bar', expected: '\\\\server\\share\\COM1:\\foo\\bar' },
20+
{ input: '\\\\server\\share\\path\\COM1:', expected: '\\\\server\\share\\path\\COM1:' },
21+
{ input: '\\\\server\\share\\COM1:..\\..\\..\\..\\Windows', expected: '\\\\server\\share\\Windows' },
22+
{ input: '\\\\server\\share\\path\\to\\LPT9:..\\..\\..\\..\\..\\..\\..\\..\\..\\file.txt',
23+
expected: '\\\\server\\share\\file.txt' },
24+
1225
{ input: 'CON', expected: 'CON' },
1326
{ input: 'con', expected: 'con' },
1427
{ input: 'CON:', expected: '.\\CON:.' },
@@ -81,6 +94,8 @@ const normalizeDeviceNameTests = [
8194
// Test cases from original vulnerability reports or similar scenarios
8295
{ input: 'COM1:.\\..\\..\\foo.js', expected: '.\\COM1:..\\..\\foo.js' },
8396
{ input: 'LPT1:.\\..\\..\\another.txt', expected: '.\\LPT1:..\\..\\another.txt' },
97+
// UNC paths
98+
{ input: '\\\\?\\COM1:.\\..\\..\\foo2.js', expected: '\\\\?\\COM1:\\foo2.js' },
8499

85100
// Paths with device names not at the beginning
86101
{ input: 'C:\\CON', expected: 'C:\\CON' },

0 commit comments

Comments
 (0)