-
-
Save bpasero/f7cf27c531146267706786e56234b8d6 to your computer and use it in GitHub Desktop.
VS Code CVE-2023-29338
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
diff --git a/lib/internal/bootstrap/pre_execution.js b/lib/internal/bootstrap/pre_execution.js | |
index 337b95133bc..8d6ed7b85d5 100644 | |
--- a/lib/internal/bootstrap/pre_execution.js | |
+++ b/lib/internal/bootstrap/pre_execution.js | |
@@ -5,7 +5,9 @@ const { | |
ObjectDefineProperty, | |
ObjectGetOwnPropertyDescriptor, | |
SafeMap, | |
+ SafeSet, | |
SafeWeakMap, | |
+ StringPrototypeSplit, | |
StringPrototypeStartsWith, | |
globalThis, | |
} = primordials; | |
@@ -81,6 +83,7 @@ function prepareMainThreadExecution(expandArgv1 = false, | |
initializeSourceMapsHandlers(); | |
initializeDeprecations(); | |
initializeWASI(); | |
+ setupAllowedUNCHosts(); | |
require('internal/v8/startup_snapshot').runDeserializeCallbacks(); | |
@@ -118,6 +121,11 @@ function patchProcessObject(expandArgv1) { | |
process._exiting = false; | |
process.argv[0] = process.execPath; | |
+ const uncHostAllowlist = new SafeSet(); | |
+ ObjectDefineProperty(process, 'uncHostAllowlist', { | |
+ value: uncHostAllowlist | |
+ }); | |
+ | |
if (expandArgv1 && process.argv[1] && | |
!StringPrototypeStartsWith(process.argv[1], '-')) { | |
// Expand process.argv[1] into a full path. | |
@@ -584,6 +592,21 @@ function loadPreloadModules() { | |
} | |
} | |
+function setupAllowedUNCHosts() { | |
+ if (process.env.NODE_UNC_HOST_ALLOWLIST) { | |
+ const { validateString } = require('internal/validators'); | |
+ const envAllowlist = process.env.NODE_UNC_HOST_ALLOWLIST; | |
+ validateString(envAllowlist, 'UNCHostAllowlist'); | |
+ const uncHostAllowlist = StringPrototypeSplit(envAllowlist, '\\'); | |
+ // Clear current allowlist. | |
+ process.uncHostAllowlist.clear(); | |
+ for (let i = 0; i < uncHostAllowlist.length; i++) { | |
+ process.uncHostAllowlist.add(uncHostAllowlist[i]); | |
+ } | |
+ delete process.env.NODE_UNC_HOST_ALLOWLIST; | |
+ } | |
+} | |
+ | |
module.exports = { | |
refreshRuntimeOptions, | |
patchProcessObject, | |
diff --git a/lib/internal/child_process.js b/lib/internal/child_process.js | |
index cba612823d0..772c0454c59 100644 | |
--- a/lib/internal/child_process.js | |
+++ b/lib/internal/child_process.js | |
@@ -1,7 +1,9 @@ | |
'use strict'; | |
const { | |
+ ArrayFrom, | |
ArrayIsArray, | |
+ ArrayPrototypeJoin, | |
ArrayPrototypePush, | |
ArrayPrototypeReduce, | |
ArrayPrototypeSlice, | |
@@ -375,6 +377,23 @@ ChildProcess.prototype.spawn = function(options) { | |
`NODE_CHANNEL_SERIALIZATION_MODE=${serialization}`); | |
} | |
+ if (process.uncHostAllowlist.size > 0) { | |
+ // Let child process know about UNC allowlist | |
+ for (const host of process.uncHostAllowlist) { | |
+ if (typeof host !== 'string') { | |
+ process.uncHostAllowlist.delete(host); | |
+ } | |
+ } | |
+ if (options.envPairs === undefined) | |
+ options.envPairs = []; | |
+ else | |
+ validateArray(options.envPairs, 'options.envPairs'); | |
+ const uncHostAllowlist = | |
+ ArrayPrototypeJoin(ArrayFrom(process.uncHostAllowlist), '\\'); | |
+ ArrayPrototypePush(options.envPairs, | |
+ `NODE_UNC_HOST_ALLOWLIST=${uncHostAllowlist}`); | |
+ } | |
+ | |
validateString(options.file, 'options.file'); | |
this.spawnfile = options.file; | |
diff --git a/lib/internal/errors.js b/lib/internal/errors.js | |
index b3a505aae8d..09d7da12633 100644 | |
--- a/lib/internal/errors.js | |
+++ b/lib/internal/errors.js | |
@@ -1637,6 +1637,9 @@ E('ERR_UNCAUGHT_EXCEPTION_CAPTURE_ALREADY_SET', | |
'`process.setupUncaughtExceptionCapture()` was called while a capture ' + | |
'callback was already active', | |
Error); | |
+E('ERR_UNC_HOST_NOT_ALLOWED', function(hostname) { | |
+ return `UNC host '${hostname}' access is not allowed`; | |
+}, Error); | |
E('ERR_UNESCAPED_CHARACTERS', '%s contains unescaped characters', TypeError); | |
E('ERR_UNHANDLED_ERROR', | |
// Using a default argument here is important so the argument is not counted | |
diff --git a/lib/internal/fs/utils.js b/lib/internal/fs/utils.js | |
index cba64694278..675fb440983 100644 | |
--- a/lib/internal/fs/utils.js | |
+++ b/lib/internal/fs/utils.js | |
@@ -18,8 +18,11 @@ const { | |
ReflectApply, | |
ReflectOwnKeys, | |
RegExpPrototypeSymbolReplace, | |
+ StringPrototypeCharCodeAt, | |
StringPrototypeEndsWith, | |
StringPrototypeIncludes, | |
+ StringPrototypeSlice, | |
+ StringPrototypeToLowerCase, | |
Symbol, | |
TypedArrayPrototypeIncludes, | |
} = primordials; | |
@@ -32,7 +35,8 @@ const { | |
ERR_INCOMPATIBLE_OPTION_PAIR, | |
ERR_INVALID_ARG_TYPE, | |
ERR_INVALID_ARG_VALUE, | |
- ERR_OUT_OF_RANGE | |
+ ERR_OUT_OF_RANGE, | |
+ ERR_UNC_HOST_NOT_ALLOWED | |
}, | |
hideStackFrames, | |
uvException | |
@@ -105,6 +109,13 @@ const { | |
} | |
} = internalBinding('constants'); | |
+const { | |
+ CHAR_FORWARD_SLASH, | |
+ CHAR_BACKWARD_SLASH, | |
+ CHAR_QUESTION_MARK, | |
+ CHAR_DOT, | |
+} = require('internal/constants'); | |
+ | |
// The access modes can be any of F_OK, R_OK, W_OK or X_OK. Some might not be | |
// available on specific systems. They can be used in combination as well | |
// (F_OK | R_OK | W_OK | X_OK). | |
@@ -144,6 +155,10 @@ const kMaxUserId = 2 ** 32 - 1; | |
const isWindows = process.platform === 'win32'; | |
+function isPathSeparator(code) { | |
+ return code === CHAR_FORWARD_SLASH || code === CHAR_BACKWARD_SLASH; | |
+} | |
+ | |
let fs; | |
function lazyLoadFs() { | |
if (!fs) { | |
@@ -669,7 +684,9 @@ const validateOffsetLengthWrite = hideStackFrames( | |
); | |
const validatePath = hideStackFrames((path, propName = 'path') => { | |
- if (typeof path !== 'string' && !isUint8Array(path)) { | |
+ const pathIsString = typeof path === 'string'; | |
+ const pathIsUint8Array = isUint8Array(path); | |
+ if (!pathIsString && !pathIsUint8Array) { | |
throw new ERR_INVALID_ARG_TYPE(propName, ['string', 'Buffer', 'URL'], path); | |
} | |
@@ -678,6 +695,76 @@ const validatePath = hideStackFrames((path, propName = 'path') => { | |
if (err !== undefined) { | |
throw err; | |
} | |
+ | |
+ const len = path.length; | |
+ let hostname = ''; | |
+ | |
+ if (isWindows && pathIsString && !pathIsUint8Array && | |
+ isPathSeparator(StringPrototypeCharCodeAt(path, 0))) { | |
+ // Possible UNC root | |
+ if (isPathSeparator(StringPrototypeCharCodeAt(path, 1))) { | |
+ // Matched double path separator at beginning | |
+ let j = 2; | |
+ let last = j; | |
+ // Match 1 or more non-path separators | |
+ while (j < len && | |
+ !isPathSeparator(StringPrototypeCharCodeAt(path, j))) { | |
+ j++; | |
+ } | |
+ if (j === len || j !== last) { | |
+ hostname = StringPrototypeSlice(path, last, j); | |
+ if (hostname.length === 1 && | |
+ (StringPrototypeCharCodeAt(hostname, 0) === CHAR_QUESTION_MARK || | |
+ StringPrototypeCharCodeAt(hostname, 0) === CHAR_DOT)) { | |
+ // Possible device root! | |
+ last = j; | |
+ // Match 1 or more path separators | |
+ while (j < len && | |
+ isPathSeparator(StringPrototypeCharCodeAt(path, j))) { | |
+ j++; | |
+ } | |
+ if (j < len && j !== last) { | |
+ // Possible long UNC path! | |
+ last = j; | |
+ // Match 1 or more non-path separators | |
+ while (j < len && | |
+ !isPathSeparator(StringPrototypeCharCodeAt(path, j))) { | |
+ j++; | |
+ } | |
+ if (StringPrototypeToLowerCase(StringPrototypeSlice(path, last, j)) === 'unc') { | |
+ // UNC path! | |
+ last = j; | |
+ // Match 1 or more path separators | |
+ while (j < len && | |
+ isPathSeparator(StringPrototypeCharCodeAt(path, j))) { | |
+ j++; | |
+ } | |
+ if (j < len && j !== last) { | |
+ last = j; | |
+ // Match 1 or more non-path separators | |
+ while (j < len && | |
+ !isPathSeparator(StringPrototypeCharCodeAt(path, j))) { | |
+ j++; | |
+ } | |
+ if (j === len || j !== last) { | |
+ hostname = StringPrototypeSlice(path, last, j); | |
+ // We matched a UNC root | |
+ if (!process.uncHostAllowlist.has(hostname)) { | |
+ throw new ERR_UNC_HOST_NOT_ALLOWED(hostname); | |
+ } | |
+ } | |
+ } | |
+ } | |
+ } | |
+ } else { | |
+ // We matched a UNC root | |
+ if (!process.uncHostAllowlist.has(hostname)) { | |
+ throw new ERR_UNC_HOST_NOT_ALLOWED(hostname); | |
+ } | |
+ } | |
+ } | |
+ } | |
+ } | |
}); | |
const getValidatedPath = hideStackFrames((fileURLOrPath, propName = 'path') => { | |
diff --git a/test/fixtures/child-process-unc-access.js b/test/fixtures/child-process-unc-access.js | |
new file mode 100644 | |
index 00000000000..5475c2c0dc5 | |
--- /dev/null | |
+++ b/test/fixtures/child-process-unc-access.js | |
@@ -0,0 +1,16 @@ | |
+const assert = require('assert'); | |
+const fs = require('fs'); | |
+ | |
+const mode = fs.R_OK | fs.W_OK; | |
+if (process.argv[2] === 'allowed') { | |
+ try { | |
+ fs.accessSync('\\\\server\\share\\dir\\file.ext', mode); | |
+ } catch (err) { | |
+ assert.strictEqual(err.message, "UNKNOWN: unknown error, access '\\\\server\\share\\dir\\file.ext'"); | |
+ } | |
+ assert.throws(() => fs.accessSync('\\\\server2\\share\\dir\\file.ext', mode), { code: 'ERR_UNC_HOST_NOT_ALLOWED' }); | |
+} else { | |
+ assert.throws(() => fs.accessSync('\\\\server\\share\\dir\\file.ext', mode), { code: 'ERR_UNC_HOST_NOT_ALLOWED' }); | |
+} | |
+ | |
+process.send({ foo: 'bar' }); | |
diff --git a/test/parallel/test-fs-readfilesync-enoent.js b/test/parallel/test-fs-readfilesync-enoent.js | |
index baf87ff990b..404069cc80d 100644 | |
--- a/test/parallel/test-fs-readfilesync-enoent.js | |
+++ b/test/parallel/test-fs-readfilesync-enoent.js | |
@@ -23,6 +23,8 @@ function test(p) { | |
})); | |
} | |
+process.uncHostAllowlist.add(os.hostname()); | |
+ | |
test(`//${os.hostname()}/c$/Windows/System32`); | |
test(`//${os.hostname()}/c$/Windows`); | |
test(`//${os.hostname()}/c$/`); | |
diff --git a/test/parallel/test-fs-unc-access.js b/test/parallel/test-fs-unc-access.js | |
new file mode 100644 | |
index 00000000000..9d3f65b2e9f | |
--- /dev/null | |
+++ b/test/parallel/test-fs-unc-access.js | |
@@ -0,0 +1,121 @@ | |
+'use strict'; | |
+const common = require('../common'); | |
+const fixtures = require('../common/fixtures'); | |
+ | |
+const assert = require('assert'); | |
+const fs = require('fs'); | |
+const fork = require('child_process').fork; | |
+ | |
+const mode = fs.R_OK | fs.W_OK; | |
+ | |
+// Not allowed by default UNC and long UNC paths | |
+assert.throws(() => fs.accessSync('\\\\server\\share\\dir\\file.ext', mode), { code: 'ERR_UNC_HOST_NOT_ALLOWED' }); | |
+assert.throws(() => fs.accessSync('\\\\server', mode), { code: 'ERR_UNC_HOST_NOT_ALLOWED' }); | |
+assert.throws(() => fs.accessSync('\\\\UNC\\', mode), { code: 'ERR_UNC_HOST_NOT_ALLOWED' }); | |
+assert.throws(() => fs.accessSync('\\\\UNC\\server\\share\\dir\\file.ext', mode), { code: 'ERR_UNC_HOST_NOT_ALLOWED' }); | |
+assert.throws(() => fs.accessSync('\\\\?\\UNC\\server\\share\\dir\\file.ext', mode), { code: 'ERR_UNC_HOST_NOT_ALLOWED' }); | |
+assert.throws(() => fs.accessSync('\\\\.\\UNC\\server\\share\\dir\\file.ext', mode), { code: 'ERR_UNC_HOST_NOT_ALLOWED' }); | |
+ | |
+// Valid long paths | |
+try { | |
+ fs.accessSync('\\\\?\\share\\dir\\file.ext', mode); | |
+} catch (err) { | |
+ assert.strictEqual(err.message, "ENOENT: no such file or directory, access '\\\\?\\share\\dir\\file.ext'"); | |
+} | |
+try { | |
+ fs.accessSync('\\\\?\\UNC', mode); | |
+} catch (err) { | |
+ assert.strictEqual(err.message, "EISDIR: illegal operation on a directory, access '\\\\?\\UNC'"); | |
+} | |
+try { | |
+ fs.accessSync('\\\\?\\UNC\\', mode); | |
+} catch (err) { | |
+ assert.strictEqual(err.message, "ENOENT: no such file or directory, access '\\\\?\\UNC\\'"); | |
+} | |
+try { | |
+ fs.accessSync('\\\\?\\', mode); | |
+} catch (err) { | |
+ assert.strictEqual(err.message, "ENOENT: no such file or directory, access '\\\\?\\'"); | |
+} | |
+try { | |
+ fs.accessSync('\\\\?', mode); | |
+} catch (err) { | |
+ assert.strictEqual(err.message, "ENOENT: no such file or directory, access '\\\\?'"); | |
+} | |
+try { | |
+ fs.accessSync('\\\\.\\share\\dir\\file.ext', mode); | |
+} catch (err) { | |
+ assert.strictEqual(err.message, "ENOENT: no such file or directory, access '\\\\.\\share\\dir\\file.ext'"); | |
+} | |
+try { | |
+ fs.accessSync('\\\\.\\UNCshare\\dir\\file.ext', mode); | |
+} catch (err) { | |
+ assert.strictEqual(err.message, "ENOENT: no such file or directory, access '\\\\.\\UNCshare\\dir\\file.ext'"); | |
+} | |
+try { | |
+ fs.accessSync('\\\\.\\UNC', mode); | |
+} catch (err) { | |
+ assert.strictEqual(err.message, "EISDIR: illegal operation on a directory, access '\\\\.\\UNC'"); | |
+} | |
+try { | |
+ fs.accessSync('\\\\.\\UNC\\', mode); | |
+} catch (err) { | |
+ assert.strictEqual(err.message, "ENOENT: no such file or directory, access '\\\\.\\UNC\\'"); | |
+} | |
+try { | |
+ fs.accessSync('\\\\.\\', mode); | |
+} catch (err) { | |
+ assert.strictEqual(err.message, "ENOENT: no such file or directory, access '\\\\.\\'"); | |
+} | |
+try { | |
+ fs.accessSync('\\\\.', mode); | |
+} catch (err) { | |
+ assert.strictEqual(err.message, "ENOENT: no such file or directory, access '\\\\.'"); | |
+} | |
+ | |
+const n = fork(fixtures.path('child-process-unc-access.js')); | |
+n.on('message', (m) => { | |
+ assert.ok(m.foo); | |
+}); | |
+n.on('exit', common.mustCall((c) => { | |
+ assert.strictEqual(c, 0); | |
+})); | |
+ | |
+process.uncHostAllowlist.add('server').add(42); | |
+ | |
+try { | |
+ fs.accessSync('\\\\server\\share\\dir\\file.ext', mode); | |
+} catch (err) { | |
+ assert.strictEqual(err.message, "UNKNOWN: unknown error, access '\\\\server\\share\\dir\\file.ext'"); | |
+} | |
+try { | |
+ fs.accessSync('\\\\server', mode); | |
+} catch (err) { | |
+ assert.strictEqual(err.message, "ENOENT: no such file or directory, access '\\\\server'"); | |
+} | |
+try { | |
+ fs.accessSync('\\\\?\\UNC\\server\\share\\dir\\file.ext', mode); | |
+} catch (err) { | |
+ assert.strictEqual(err.message, "UNKNOWN: unknown error, access '\\\\?\\UNC\\server\\share\\dir\\file.ext'"); | |
+} | |
+try { | |
+ fs.accessSync('\\\\.\\UNC\\server\\share\\dir\\file.ext', mode); | |
+} catch (err) { | |
+ assert.strictEqual(err.message, "UNKNOWN: unknown error, access '\\\\.\\UNC\\server\\share\\dir\\file.ext'"); | |
+} | |
+assert.throws(() => fs.accessSync('\\\\server2\\share\\dir\\file.ext', mode), { code: 'ERR_UNC_HOST_NOT_ALLOWED' }); | |
+assert.throws(() => fs.accessSync('\\\\UNC\\server\\share\\dir\\file.ext', mode), { code: 'ERR_UNC_HOST_NOT_ALLOWED' }); | |
+assert.throws(() => fs.accessSync('\\\\UNC\\', mode), { code: 'ERR_UNC_HOST_NOT_ALLOWED' }); | |
+ | |
+const n2 = fork(fixtures.path('child-process-unc-access.js'), ['allowed']); | |
+n2.on('message', (m) => { | |
+ assert.ok(m.foo); | |
+}); | |
+n2.on('exit', common.mustCall((c) => { | |
+ assert.strictEqual(c, 0); | |
+})); | |
+ | |
+const propDesc = Object.getOwnPropertyDescriptor(process, 'uncHostAllowlist'); | |
+assert.strictEqual(propDesc.writable, false, "must not be writable"); | |
+assert.strictEqual(propDesc.enumerable, false, "must not be enumerable"); | |
+assert.strictEqual(propDesc.configurable, false, "must not be configurable"); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Good Job