diff --git a/lib/internal/worker.js b/lib/internal/worker.js index 60e95273ef5..24452763a29 100644 --- a/lib/internal/worker.js +++ b/lib/internal/worker.js @@ -5,6 +5,7 @@ const { ArrayPrototypeForEach, ArrayPrototypeMap, ArrayPrototypePush, + ArrayPrototypeSlice, AtomicsAdd, Float64Array, FunctionPrototypeBind, @@ -20,6 +21,7 @@ const { SafeMap, String, StringPrototypeTrim, + StringPrototypeStartsWith, Symbol, SymbolAsyncDispose, SymbolFor, @@ -111,6 +113,8 @@ let debug = require('internal/util/debuglog').debuglog('worker', (fn) => { const dc = require('diagnostics_channel'); const workerThreadsChannel = dc.channel('worker_threads'); +const permission = require('internal/process/permission'); + let cwdCounter; let normalizeHeapProfileOptions; let normalizeCpuProfileOptions; @@ -204,6 +208,57 @@ class HeapProfileHandle { } } + +function ensurePermissionFlagsInExecArgv(execArgv) { + if (!permission.isEnabled() || execArgv == null) { + return execArgv; + } + + const flagsToCopy = [ + ...permission.availableFlags(), + '--permission', + '--permission-audit', + ]; + const out = ArrayPrototypeSlice(execArgv); + + function indexOfFlag(token) { + for (let i = 0; i < out.length; i++) { + const a = out[i]; + if (a === token) { + return i; + } + if (StringPrototypeStartsWith(a, `${token}=`)) { + return i; + } + } + return -1; + } + + for (let i = 0; i < process.execArgv.length; i++) { + const arg = process.execArgv[i]; + for (let j = 0; j < flagsToCopy.length; j++) { + const flag = flagsToCopy[j]; + if (arg === flag) { + if (indexOfFlag(flag) === -1) { + ArrayPrototypePush(out, arg); + const next = process.execArgv[i + 1]; + if (next !== undefined && !StringPrototypeStartsWith(next, '-')) { + ArrayPrototypePush(out, next); + } + } + break; + } + if (StringPrototypeStartsWith(arg, `${flag}=`)) { + if (indexOfFlag(flag) === -1) { + ArrayPrototypePush(out, arg); + } + break; + } + } + } + return out; +} + class Worker extends EventEmitter { constructor(filename, options = kEmptyObject) { throwIfBuildingSnapshot('Creating workers'); @@ -218,6 +273,10 @@ class Worker extends EventEmitter { if (options.execArgv) validateArray(options.execArgv, 'options.execArgv'); + let workerExecArgv = options.execArgv; + if (workerExecArgv) + workerExecArgv = ensurePermissionFlagsInExecArgv(workerExecArgv); + let argv; if (options.argv) { validateArray(options.argv, 'options.argv'); @@ -288,7 +347,7 @@ class Worker extends EventEmitter { // Set up the C++ handle for the worker, as well as some internal wiring. this[kHandle] = new WorkerImpl(url, env === process.env ? null : env, - options.execArgv, + workerExecArgv, parseResourceLimits(options.resourceLimits), !!(options.trackUnmanagedFds ?? true), isInternal, diff --git a/test/parallel/test-permission-worker-empty-execargv.js b/test/parallel/test-permission-worker-empty-execargv.js new file mode 100644 index 00000000000..2b02a574166 --- /dev/null +++ b/test/parallel/test-permission-worker-empty-execargv.js @@ -0,0 +1,93 @@ +'use strict'; + +// Consistency under the Permission Model: +// Worker with explicit execArgv: [] must keep the same filesystem allowlist +// as a default Worker (empty/modified execArgv must not drop parent limits). + +const common = require('../common'); +const { isMainThread } = require('worker_threads'); + +if (!isMainThread) { + common.skip('This test only works on a main thread'); +} + +const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); +const { spawnSync } = require('child_process'); +const tmpdir = require('../common/tmpdir'); + +tmpdir.refresh(); + +const allowed = tmpdir.path; +const deniedFile = path.join(tmpdir.path, '..', 'permission-worker-denied-file'); +fs.writeFileSync(deniedFile, 'secret\n'); + +const workerSource = ` + const { parentPort } = require('worker_threads'); + const fs = require('fs'); + const denied = ${JSON.stringify(deniedFile)}; + let result; + try { + result = { ok: true, data: fs.readFileSync(denied, 'utf8') }; + } catch (err) { + result = { ok: false, code: err.code, message: err.message }; + } + parentPort.postMessage(result); +`; + +function runCase(label, useEmptyExecArgv) { + const execArgvLine = useEmptyExecArgv ? 'execArgv: [],' : ''; + const code = ` + const { Worker } = require('worker_threads'); + const w = new Worker(${JSON.stringify(workerSource)}, { + eval: true, + ${execArgvLine} + }); + w.on('message', (msg) => { + process.stdout.write(JSON.stringify({ label: ${JSON.stringify(label)}, msg }) + '\\n'); + process.exit(0); + }); + w.on('error', (err) => { + console.error(err); + process.exit(1); + }); + `; + return spawnSync( + process.execPath, + [ + '--permission', + `--allow-fs-read=${allowed}`, + '--allow-worker', + '-e', + code, + ], + { + encoding: 'utf8', + timeout: 15000, + }, + ); +} + +const defaultWorker = runCase('default', false); +const emptyExecArgv = runCase('empty-execArgv', true); + +assert.strictEqual(defaultWorker.status, 0, defaultWorker.stderr); +assert.strictEqual(emptyExecArgv.status, 0, emptyExecArgv.stderr); + +const defaultMsg = JSON.parse(defaultWorker.stdout.trim().split('\n').pop()); +const emptyMsg = JSON.parse(emptyExecArgv.stdout.trim().split('\n').pop()); + +assert.strictEqual( + defaultMsg.msg.ok, + false, + `default Worker should deny: ${JSON.stringify(defaultMsg)}`, +); +assert.strictEqual(defaultMsg.msg.code, 'ERR_ACCESS_DENIED'); + +assert.strictEqual( + emptyMsg.msg.ok, + false, + `Worker with execArgv: [] should deny: ${JSON.stringify(emptyMsg)}`, +); +assert.strictEqual(emptyMsg.msg.code, 'ERR_ACCESS_DENIED');