diff --git a/CHANGELOG.md b/CHANGELOG.md index 43880847dd26ad..310b540976f536 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,7 +37,8 @@ release. -22.17.0
+22.17.1
+22.17.0
22.16.0
22.15.1
22.15.0
diff --git a/common.gypi b/common.gypi index a73d4401f26d84..b88371ec13da5a 100644 --- a/common.gypi +++ b/common.gypi @@ -38,7 +38,7 @@ # Reset this number to 0 on major V8 upgrades. # Increment by one for each non-official patch applied to deps/v8. - 'v8_embedder_string': '-node.26', + 'v8_embedder_string': '-node.27', ##### V8 defaults for Node.js ##### diff --git a/deps/v8/src/heap/cppgc/marking-state.h b/deps/v8/src/heap/cppgc/marking-state.h index 4ce1ce4074151b..3153407f4f25bf 100644 --- a/deps/v8/src/heap/cppgc/marking-state.h +++ b/deps/v8/src/heap/cppgc/marking-state.h @@ -342,7 +342,7 @@ class MutatorMarkingState final : public BasicMarkingState { ~MutatorMarkingState() override = default; inline bool MarkNoPush(HeapObjectHeader& header) { - return MutatorMarkingState::BasicMarkingState::MarkNoPush(header); + return BasicMarkingState::MarkNoPush(header); } inline void ReTraceMarkedWeakContainer(cppgc::Visitor&, HeapObjectHeader&); diff --git a/doc/changelogs/CHANGELOG_V22.md b/doc/changelogs/CHANGELOG_V22.md index 6205d3dcf14818..02a0f2d76c0b86 100644 --- a/doc/changelogs/CHANGELOG_V22.md +++ b/doc/changelogs/CHANGELOG_V22.md @@ -9,6 +9,7 @@ +22.17.1
22.17.0
22.16.0
22.15.1
@@ -61,6 +62,21 @@ * [io.js](CHANGELOG_IOJS.md) * [Archive](CHANGELOG_ARCHIVE.md) + + +## 2025-07-15, Version 22.17.1 'Jod' (LTS), @RafaelGSS + +This is a security release. + +### Notable Changes + +* (CVE-2025-27210) Windows Device Names (CON, PRN, AUX) Bypass Path Traversal Protection in path.normalize() + +### Commits + +* \[[`8cf5d66ab7`](https://github.com/nodejs/node/commit/8cf5d66ab7)] - **(CVE-2025-27210)** **lib**: handle all windows reserved driver name (RafaelGSS) [nodejs-private/node-private#721](https://github.com/nodejs-private/node-private/pull/721) +* \[[`9c0cb487ec`](https://github.com/nodejs/node/commit/9c0cb487ec)] - **win,build**: fix MSVS v17.14 compilation issue (StefanStojanovic) [#58902](https://github.com/nodejs/node/pull/58902) + ## 2025-06-24, Version 22.17.0 'Jod' (LTS), @aduh95 diff --git a/lib/path.js b/lib/path.js index e35147f4b2e159..d39f67e75cd6b0 100644 --- a/lib/path.js +++ b/lib/path.js @@ -22,6 +22,7 @@ 'use strict'; const { + ArrayPrototypeIncludes, ArrayPrototypeJoin, ArrayPrototypeSlice, FunctionPrototypeBind, @@ -34,6 +35,7 @@ const { StringPrototypeSlice, StringPrototypeSplit, StringPrototypeToLowerCase, + StringPrototypeToUpperCase, } = primordials; const { @@ -68,6 +70,17 @@ function isPosixPathSeparator(code) { return code === CHAR_FORWARD_SLASH; } +const WINDOWS_RESERVED_NAMES = [ + 'CON', 'PRN', 'AUX', 'NUL', + 'COM1', 'COM2', 'COM3', 'COM4', 'COM5', 'COM6', 'COM7', 'COM8', 'COM9', + 'LPT1', 'LPT2', 'LPT3', 'LPT4', 'LPT5', 'LPT6', 'LPT7', 'LPT8', 'LPT9', +]; + +function isWindowsReservedName(path, colonIndex) { + const devicePart = StringPrototypeToUpperCase(StringPrototypeSlice(path, 0, colonIndex)); + return ArrayPrototypeIncludes(WINDOWS_RESERVED_NAMES, devicePart); +} + function isWindowsDeviceRoot(code) { return (code >= CHAR_UPPERCASE_A && code <= CHAR_UPPERCASE_Z) || (code >= CHAR_LOWERCASE_A && code <= CHAR_LOWERCASE_Z); @@ -406,16 +419,21 @@ const win32 = { } else { rootEnd = 1; } - } else if (isWindowsDeviceRoot(code) && - StringPrototypeCharCodeAt(path, 1) === CHAR_COLON) { - // Possible device root - device = StringPrototypeSlice(path, 0, 2); - rootEnd = 2; - if (len > 2 && isPathSeparator(StringPrototypeCharCodeAt(path, 2))) { - // Treat separator following drive name as an absolute path - // indicator - isAbsolute = true; - rootEnd = 3; + } else { + const colonIndex = StringPrototypeIndexOf(path, ':'); + if (colonIndex > 0) { + if (isWindowsDeviceRoot(code) && colonIndex === 1) { + device = StringPrototypeSlice(path, 0, 2); + rootEnd = 2; + if (len > 2 && isPathSeparator(StringPrototypeCharCodeAt(path, 2))) { + isAbsolute = true; + rootEnd = 3; + } + } else if (isWindowsReservedName(path, colonIndex)) { + device = StringPrototypeSlice(path, 0, colonIndex + 1); + rootEnd = colonIndex + 1; + + } } } @@ -439,12 +457,17 @@ const win32 = { return `.\\${tail}`; } let index = StringPrototypeIndexOf(path, ':'); + do { if (index === len - 1 || isPathSeparator(StringPrototypeCharCodeAt(path, index + 1))) { return `.\\${tail}`; } } while ((index = StringPrototypeIndexOf(path, ':', index + 1)) !== -1); } + const colonIndex = StringPrototypeIndexOf(path, ':'); + if (isWindowsReservedName(path, colonIndex)) { + return `.\\${device ?? ''}${tail}`; + } if (device === undefined) { return isAbsolute ? `\\${tail}` : tail; } diff --git a/src/node_version.h b/src/node_version.h index e201313e65678e..fb70e47f5964b4 100644 --- a/src/node_version.h +++ b/src/node_version.h @@ -24,7 +24,7 @@ #define NODE_MAJOR_VERSION 22 #define NODE_MINOR_VERSION 17 -#define NODE_PATCH_VERSION 0 +#define NODE_PATCH_VERSION 1 #define NODE_VERSION_IS_LTS 1 #define NODE_VERSION_LTS_CODENAME "Jod" diff --git a/test/parallel/test-path-win32-normalize-device-names.js b/test/parallel/test-path-win32-normalize-device-names.js new file mode 100644 index 00000000000000..927bc5cec8a2e5 --- /dev/null +++ b/test/parallel/test-path-win32-normalize-device-names.js @@ -0,0 +1,98 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const path = require('path'); + +if (!common.isWindows) { + common.skip('Windows only'); +} + +const normalizeDeviceNameTests = [ + { input: 'CON', expected: 'CON' }, + { input: 'con', expected: 'con' }, + { input: 'CON:', expected: '.\\CON:.' }, + { input: 'con:', expected: '.\\con:.' }, + { input: 'CON:.', expected: '.\\CON:.' }, + { input: 'coN:', expected: '.\\coN:.' }, + { input: 'LPT9.foo', expected: 'LPT9.foo' }, + { input: 'COM9:', expected: '.\\COM9:.' }, + { input: 'COM9.', expected: '.\\COM9.' }, + { input: 'C:COM9', expected: 'C:COM9' }, + { input: 'C:\\COM9', expected: 'C:\\COM9' }, + { input: 'CON:./foo', expected: '.\\CON:foo' }, + { input: 'CON:/foo', expected: '.\\CON:foo' }, + { input: 'CON:../foo', expected: '.\\CON:..\\foo' }, + { input: 'CON:/../foo', expected: '.\\CON:..\\foo' }, + { input: 'CON:./././foo', expected: '.\\CON:foo' }, + { input: 'CON:..', expected: '.\\CON:..' }, + { input: 'CON:..\\', expected: '.\\CON:..\\' }, + { input: 'CON:..\\..', expected: '.\\CON:..\\..' }, + { input: 'CON:..\\..\\', expected: '.\\CON:..\\..\\' }, + { input: 'CON:..\\..\\foo', expected: '.\\CON:..\\..\\foo' }, + { input: 'CON:..\\..\\foo\\', expected: '.\\CON:..\\..\\foo\\' }, + { input: 'CON:..\\..\\foo\\bar', expected: '.\\CON:..\\..\\foo\\bar' }, + { input: 'CON:..\\..\\foo\\bar\\', expected: '.\\CON:..\\..\\foo\\bar\\' }, + { input: 'COM1:a:b:c', expected: '.\\COM1:a:b:c' }, + { input: 'COM1:a:b:c/', expected: '.\\COM1:a:b:c\\' }, + { input: 'c:lpt1', expected: 'c:lpt1' }, + { input: 'c:\\lpt1', expected: 'c:\\lpt1' }, + + // Reserved device names with path traversal + { input: 'CON:.\\..\\..\\foo', expected: '.\\CON:..\\..\\foo' }, + { input: 'PRN:.\\..\\bar', expected: '.\\PRN:..\\bar' }, + { input: 'AUX:/../../baz', expected: '.\\AUX:..\\..\\baz' }, + + { input: 'COM1:', expected: '.\\COM1:.' }, + { input: 'COM9:', expected: '.\\COM9:.' }, + { input: 'COM1:.\\..\\..\\foo', expected: '.\\COM1:..\\..\\foo' }, + { input: 'LPT1:', expected: '.\\LPT1:.' }, + { input: 'LPT9:', expected: '.\\LPT9:.' }, + { input: 'LPT1:.\\..\\..\\foo', expected: '.\\LPT1:..\\..\\foo' }, + { input: 'LpT5:/another/path', expected: '.\\LpT5:another\\path' }, + + { input: 'C:\\foo', expected: 'C:\\foo' }, + { input: 'D:bar', expected: 'D:bar' }, + + { input: 'CON', expected: 'CON' }, + { input: 'CON.TXT', expected: 'CON.TXT' }, + { input: 'COM10:', expected: '.\\COM10:' }, + { input: 'LPT10:', expected: '.\\LPT10:' }, + { input: 'CONNINGTOWER:', expected: '.\\CONNINGTOWER:' }, + { input: 'AUXILIARYDEVICE:', expected: '.\\AUXILIARYDEVICE:' }, + { input: 'NULLED:', expected: '.\\NULLED:' }, + { input: 'PRNINTER:', expected: '.\\PRNINTER:' }, + + { input: 'CON:\\..\\..\\windows\\system32', expected: '.\\CON:..\\..\\windows\\system32' }, + { input: 'PRN:.././../etc/passwd', expected: '.\\PRN:..\\..\\etc\\passwd' }, + + // Test with trailing slashes + { input: 'CON:\\', expected: '.\\CON:.\\' }, + { input: 'COM1:\\foo\\bar\\', expected: '.\\COM1:foo\\bar\\' }, + + // 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' }, + + // Paths with device names not at the beginning + { input: 'C:\\CON', expected: 'C:\\CON' }, + { input: 'C:\\path\\to\\COM1:', expected: 'C:\\path\\to\\COM1:' }, + + // Device name followed by multiple colons + { input: 'CON::', expected: '.\\CON::' }, + { input: 'COM1:::foo', expected: '.\\COM1:::foo' }, + + // Device name with mixed path separators + { input: 'AUX:/foo\\bar/baz', expected: '.\\AUX:foo\\bar\\baz' }, +]; + +for (const { input, expected } of normalizeDeviceNameTests) { + const actual = path.win32.normalize(input); + assert.strictEqual(actual, expected, + `path.win32.normalize(${JSON.stringify(input)}) === ${JSON.stringify(expected)}, but got ${JSON.stringify(actual)}`); +} + +assert.strictEqual(path.win32.normalize('CON:foo/../bar'), '.\\CON:bar'); + +// This should NOT be prefixed because 'c:' is treated as a drive letter. +assert.strictEqual(path.win32.normalize('c:COM1:'), 'c:COM1:');