diff --git a/lib/path.js b/lib/path.js index bee827c06e9..be6fd8b24ac 100644 --- a/lib/path.js +++ b/lib/path.js @@ -400,6 +400,13 @@ const win32 = { // We matched a device root (e.g. \\\\.\\PHYSICALDRIVE0) device = `\\\\${firstPart}`; rootEnd = 4; + const colonIndex = StringPrototypeIndexOf(path, ':'); + // Special case: handle \\?\COM1: or similar reserved device paths + const possibleDevice = StringPrototypeSlice(path, 4, colonIndex + 1); + if (isWindowsReservedName(possibleDevice, possibleDevice.length - 1)) { + device = `\\\\?\\${possibleDevice}`; + rootEnd = 4 + possibleDevice.length; + } } else if (j === len) { // We matched a UNC root only // Return the normalized version of the UNC root since there @@ -558,6 +565,36 @@ const win32 = { joined = `\\${StringPrototypeSlice(joined, slashCount)}`; } + // Skip normalization when reserved device names are present + const parts = []; + let part = ''; + + for (let i = 0; i < joined.length; i++) { + if (joined[i] === '\\') { + if (part) parts.push(part); + part = ''; + // Skip consecutive backslashes + while (i + 1 < joined.length && joined[i + 1] === '\\') i++; + } else { + part += joined[i]; + } + } + // Add the final part if any + if (part) parts.push(part); + + // Check if any part has a Windows reserved name + if (parts.some((p) => { + const colonIndex = StringPrototypeIndexOf(p, ':'); + return colonIndex !== -1 && isWindowsReservedName(p, colonIndex); + })) { + // Replace forward slashes with backslashes + let result = ''; + for (let i = 0; i < joined.length; i++) { + result += joined[i] === '/' ? '\\' : joined[i]; + } + return result; + } + return win32.normalize(joined); }, diff --git a/test/parallel/test-path-join.js b/test/parallel/test-path-join.js index b8d6375989a..ec4f5fea709 100644 --- a/test/parallel/test-path-join.js +++ b/test/parallel/test-path-join.js @@ -110,6 +110,14 @@ joinTests.push([ [['c:.', 'file'], 'c:file'], [['c:', '/'], 'c:\\'], [['c:', 'file'], 'c:\\file'], + // UNC path join tests (Windows) + [['\\server\\share', 'file.txt'], '\\server\\share\\file.txt'], + [['\\server\\share', 'folder', 'another.txt'], '\\server\\share\\folder\\another.txt'], + [['\\server\\share', 'COM1:'], '\\server\\share\\COM1:'], + [['\\server\\share', 'path', 'LPT1:'], '\\server\\share\\path\\LPT1:'], + [['\\fileserver\\public\\uploads', 'CON:..\\..\\..\\private\\db.conf'], + '\\fileserver\\public\\uploads\\CON:..\\..\\..\\private\\db.conf'], + // Path traversal in previous versions of Node.js. [['./upload', '/../C:/Windows'], '.\\C:\\Windows'], [['upload', '../', 'C:foo'], '.\\C:foo'], diff --git a/test/parallel/test-path-win32-normalize-device-names.js b/test/parallel/test-path-win32-normalize-device-names.js index 2c6dcf142a2..b34c9061e56 100644 --- a/test/parallel/test-path-win32-normalize-device-names.js +++ b/test/parallel/test-path-win32-normalize-device-names.js @@ -9,6 +9,19 @@ if (!common.isWindows) { } const normalizeDeviceNameTests = [ + // UNC paths: \\server\share\... is a Windows UNC path, where 'server' is the network server name and 'share' + // is the shared folder. These are used for network file access and are subject to reserved device name + // checks after the share. + { input: '\\\\server\\share\\COM1:', expected: '\\\\server\\share\\COM1:' }, + { input: '\\\\server\\share\\PRN:', expected: '\\\\server\\share\\PRN:' }, + { input: '\\\\server\\share\\AUX:', expected: '\\\\server\\share\\AUX:' }, + { input: '\\\\server\\share\\LPT1:', expected: '\\\\server\\share\\LPT1:' }, + { input: '\\\\server\\share\\COM1:\\foo\\bar', expected: '\\\\server\\share\\COM1:\\foo\\bar' }, + { input: '\\\\server\\share\\path\\COM1:', expected: '\\\\server\\share\\path\\COM1:' }, + { input: '\\\\server\\share\\COM1:..\\..\\..\\..\\Windows', expected: '\\\\server\\share\\Windows' }, + { input: '\\\\server\\share\\path\\to\\LPT9:..\\..\\..\\..\\..\\..\\..\\..\\..\\file.txt', + expected: '\\\\server\\share\\file.txt' }, + { input: 'CON', expected: 'CON' }, { input: 'con', expected: 'con' }, { input: 'CON:', expected: '.\\CON:.' }, @@ -81,6 +94,8 @@ const normalizeDeviceNameTests = [ // Test cases from original vulnerability reports or similar scenarios { input: 'COM1:.\\..\\..\\foo.js', expected: '.\\COM1:..\\..\\foo.js' }, { input: 'LPT1:.\\..\\..\\another.txt', expected: '.\\LPT1:..\\..\\another.txt' }, + // UNC paths + { input: '\\\\?\\COM1:.\\..\\..\\foo2.js', expected: '\\\\?\\COM1:\\foo2.js' }, // Paths with device names not at the beginning { input: 'C:\\CON', expected: 'C:\\CON' },