fix(core): Disallow code generation in task runner (#12522)
Some checks are pending
Test Master / install-and-build (push) Waiting to run
Test Master / Unit tests (18.x) (push) Blocked by required conditions
Test Master / Unit tests (20.x) (push) Blocked by required conditions
Test Master / Unit tests (22.4) (push) Blocked by required conditions
Test Master / Lint (push) Blocked by required conditions
Test Master / Notify Slack on failure (push) Blocked by required conditions

This commit is contained in:
Tomi Turtiainen 2025-01-09 13:27:17 +02:00 committed by GitHub
parent 46f13cfca9
commit 35b618098b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 23 additions and 12 deletions

View file

@ -4,7 +4,10 @@
"runner-type": "javascript", "runner-type": "javascript",
"workdir": "/home/node", "workdir": "/home/node",
"command": "/usr/local/bin/node", "command": "/usr/local/bin/node",
"args": ["/usr/local/lib/node_modules/n8n/node_modules/@n8n/task-runner/dist/start.js"], "args": [
"--disallow-code-generation-from-strings",
"/usr/local/lib/node_modules/n8n/node_modules/@n8n/task-runner/dist/start.js"
],
"allowed-env": [ "allowed-env": [
"PATH", "PATH",
"GENERIC_TIMEZONE", "GENERIC_TIMEZONE",

View file

@ -302,6 +302,7 @@ describe('JsTaskRunner', () => {
['typeof clearInterval', 'function'], ['typeof clearInterval', 'function'],
['typeof clearImmediate', 'function'], ['typeof clearImmediate', 'function'],
], ],
eval: [['eval("1+2")', 3]],
'JS built-ins': [ 'JS built-ins': [
['typeof btoa', 'function'], ['typeof btoa', 'function'],
['typeof atob', 'function'], ['typeof atob', 'function'],

View file

@ -19,7 +19,7 @@ import type {
} from 'n8n-workflow'; } from 'n8n-workflow';
import * as a from 'node:assert'; import * as a from 'node:assert';
import { inspect } from 'node:util'; import { inspect } from 'node:util';
import { runInNewContext, type Context } from 'node:vm'; import { type Context, createContext, runInContext } from 'node:vm';
import type { MainConfig } from '@/config/main-config'; import type { MainConfig } from '@/config/main-config';
import { UnsupportedFunctionError } from '@/js-task-runner/errors/unsupported-function.error'; import { UnsupportedFunctionError } from '@/js-task-runner/errors/unsupported-function.error';
@ -158,10 +158,8 @@ export class JsTaskRunner extends TaskRunner {
private getNativeVariables() { private getNativeVariables() {
return { return {
// Exposed Node.js globals in vm2 // Exposed Node.js globals
Buffer, Buffer,
Function,
eval,
setTimeout, setTimeout,
setInterval, setInterval,
setImmediate, setImmediate,
@ -205,7 +203,7 @@ export class JsTaskRunner extends TaskRunner {
signal.addEventListener('abort', abortHandler, { once: true }); signal.addEventListener('abort', abortHandler, { once: true });
const taskResult = runInNewContext( const taskResult = runInContext(
`globalThis.global = globalThis; module.exports = async function VmCodeWrapper() {${settings.code}\n}()`, `globalThis.global = globalThis; module.exports = async function VmCodeWrapper() {${settings.code}\n}()`,
context, context,
{ timeout: this.taskTimeout * 1000 }, { timeout: this.taskTimeout * 1000 },
@ -268,7 +266,7 @@ export class JsTaskRunner extends TaskRunner {
signal.addEventListener('abort', abortHandler); signal.addEventListener('abort', abortHandler);
const taskResult = runInNewContext( const taskResult = runInContext(
`module.exports = async function VmCodeWrapper() {${settings.code}\n}()`, `module.exports = async function VmCodeWrapper() {${settings.code}\n}()`,
context, context,
{ timeout: this.taskTimeout * 1000 }, { timeout: this.taskTimeout * 1000 },
@ -470,7 +468,7 @@ export class JsTaskRunner extends TaskRunner {
dataProxy: IWorkflowDataProxyData, dataProxy: IWorkflowDataProxyData,
additionalProperties: Record<string, unknown> = {}, additionalProperties: Record<string, unknown> = {},
): Context { ): Context {
const context: Context = { return createContext({
[inspect.custom]: () => '[[ExecutionContext]]', [inspect.custom]: () => '[[ExecutionContext]]',
require: this.requireResolver, require: this.requireResolver,
module: {}, module: {},
@ -480,8 +478,6 @@ export class JsTaskRunner extends TaskRunner {
...dataProxy, ...dataProxy,
...this.buildRpcCallObject(taskId), ...this.buildRpcCallObject(taskId),
...additionalProperties, ...additionalProperties,
}; });
return context;
} }
} }

View file

@ -117,5 +117,16 @@ describe('TaskRunnerProcess', () => {
const options = spawnMock.mock.calls[0][2] as SpawnOptions; const options = spawnMock.mock.calls[0][2] as SpawnOptions;
expect(options.env).not.toHaveProperty('NODE_OPTIONS'); expect(options.env).not.toHaveProperty('NODE_OPTIONS');
}); });
it('should use --disallow-code-generation-from-strings flag', async () => {
jest.spyOn(authService, 'createGrantToken').mockResolvedValue('grantToken');
await taskRunnerProcess.start();
expect(spawnMock.mock.calls[0].at(1)).toEqual([
'--disallow-code-generation-from-strings',
expect.stringContaining('/packages/@n8n/task-runner/dist/start.js'),
]);
});
}); });
}); });

View file

@ -106,7 +106,7 @@ export class TaskRunnerProcess extends TypedEmitter<TaskRunnerProcessEventMap> {
startNode(grantToken: string, taskBrokerUri: string) { startNode(grantToken: string, taskBrokerUri: string) {
const startScript = require.resolve('@n8n/task-runner/start'); const startScript = require.resolve('@n8n/task-runner/start');
return spawn('node', [startScript], { return spawn('node', ['--disallow-code-generation-from-strings', startScript], {
env: this.getProcessEnvVars(grantToken, taskBrokerUri), env: this.getProcessEnvVars(grantToken, taskBrokerUri),
}); });
} }