Skip to content

Instantly share code, notes, and snippets.

@bpasero
Created May 9, 2023 17:06
Show Gist options
  • Save bpasero/f7cf27c531146267706786e56234b8d6 to your computer and use it in GitHub Desktop.
Save bpasero/f7cf27c531146267706786e56234b8d6 to your computer and use it in GitHub Desktop.
VS Code CVE-2023-29338
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");
@insu-pl
Copy link

insu-pl commented Jun 23, 2024

Good Job

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment