diff --git a/packages/@n8n/task-runner/src/config/js-runner-config.ts b/packages/@n8n/task-runner/src/config/js-runner-config.ts index 4cba6f1d98..97d63268e5 100644 --- a/packages/@n8n/task-runner/src/config/js-runner-config.ts +++ b/packages/@n8n/task-runner/src/config/js-runner-config.ts @@ -7,4 +7,14 @@ export class JsRunnerConfig { @Env('NODE_FUNCTION_ALLOW_EXTERNAL') allowedExternalModules: string = ''; + + /** + * Whether to allow prototype mutation for external libraries. Set to `false` + * to allow modules that rely on runtime prototype mutation, e.g. `puppeteer`, + * at the cost of security. + * + * @default false + */ + @Env('N8N_RUNNERS_ALLOW_PROTOTYPE_MUTATION') + allowPrototypeMutation: boolean = false; } diff --git a/packages/@n8n/task-runner/src/js-task-runner/__tests__/js-task-runner.test.ts b/packages/@n8n/task-runner/src/js-task-runner/__tests__/js-task-runner.test.ts index 2fc048fd84..d64865a22f 100644 --- a/packages/@n8n/task-runner/src/js-task-runner/__tests__/js-task-runner.test.ts +++ b/packages/@n8n/task-runner/src/js-task-runner/__tests__/js-task-runner.test.ts @@ -30,6 +30,11 @@ import { jest.mock('ws'); const defaultConfig = new MainConfig(); +defaultConfig.jsRunnerConfig ??= { + allowedBuiltInModules: '', + allowedExternalModules: '', + allowPrototypeMutation: true, // needed for jest +}; describe('JsTaskRunner', () => { const createRunnerWithOpts = ( @@ -1435,6 +1440,31 @@ describe('JsTaskRunner', () => { // @ts-expect-error Non-existing property expect(Duration.fromObject({ hours: 1 }).maliciousKey).toBeUndefined(); }); + + it('should allow prototype mutation when `allowPrototypeMutation` is true', async () => { + const runner = createRunnerWithOpts({ + allowPrototypeMutation: true, + }); + + const outcome = await executeForAllItems({ + code: ` + const obj = {}; + + Object.prototype.maliciousProperty = 'compromised'; + + return [{ json: { + prototypeMutated: obj.maliciousProperty === 'compromised' + }}]; + `, + inputItems: [{ a: 1 }], + runner, + }); + + expect(outcome.result).toEqual([wrapIntoJson({ prototypeMutated: true })]); + + // @ts-expect-error Non-existing property + delete Object.prototype.maliciousProperty; + }); }); describe('stack trace', () => { diff --git a/packages/@n8n/task-runner/src/js-task-runner/js-task-runner.ts b/packages/@n8n/task-runner/src/js-task-runner/js-task-runner.ts index 25a8f8dfa4..40eb3d1896 100644 --- a/packages/@n8n/task-runner/src/js-task-runner/js-task-runner.ts +++ b/packages/@n8n/task-runner/src/js-task-runner/js-task-runner.ts @@ -117,10 +117,13 @@ export class JsTaskRunner extends TaskRunner { allowedExternalModules, }); - this.preventPrototypePollution(allowedExternalModules); + this.preventPrototypePollution(allowedExternalModules, jsRunnerConfig.allowPrototypeMutation); } - private preventPrototypePollution(allowedExternalModules: Set | '*') { + private preventPrototypePollution( + allowedExternalModules: Set | '*', + allowPrototypeMutation: boolean, + ) { if (allowedExternalModules instanceof Set) { // This is a workaround to enable the allowed external libraries to mutate // prototypes directly. For example momentjs overrides .toString() directly @@ -132,8 +135,8 @@ export class JsTaskRunner extends TaskRunner { } } - // Freeze globals, except for Jest - if (process.env.NODE_ENV !== 'test') { + // Freeze globals if needed + if (!allowPrototypeMutation) { Object.getOwnPropertyNames(globalThis) // @ts-expect-error globalThis does not have string in index signature // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access