fix(core): Fix usage of external libs in task runner (#12788)

Co-authored-by: Cornelius Suermann <cornelius@n8n.io>
This commit is contained in:
Tomi Turtiainen 2025-01-23 10:18:00 +02:00 committed by GitHub
parent fb662dd95c
commit 3d9d5bf9d5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 105 additions and 11 deletions

View file

@ -0,0 +1,78 @@
import { ApplicationError } from 'n8n-workflow';
import { ExecutionError } from '@/js-task-runner/errors/execution-error';
import { createRequireResolver, type RequireResolverOpts } from '../require-resolver';
describe('require resolver', () => {
let defaultOpts: RequireResolverOpts;
beforeEach(() => {
defaultOpts = {
allowedBuiltInModules: new Set(['path', 'fs']),
allowedExternalModules: new Set(['lodash']),
};
});
describe('built-in modules', () => {
it('should allow requiring whitelisted built-in modules', () => {
const resolver = createRequireResolver(defaultOpts);
expect(() => resolver('path')).not.toThrow();
expect(() => resolver('fs')).not.toThrow();
});
it('should throw when requiring non-whitelisted built-in modules', () => {
const resolver = createRequireResolver(defaultOpts);
expect(() => resolver('crypto')).toThrow(ExecutionError);
});
it('should allow all built-in modules when allowedBuiltInModules is "*"', () => {
const resolver = createRequireResolver({
...defaultOpts,
allowedBuiltInModules: '*',
});
expect(() => resolver('path')).not.toThrow();
expect(() => resolver('crypto')).not.toThrow();
expect(() => resolver('fs')).not.toThrow();
});
});
describe('external modules', () => {
it('should allow requiring whitelisted external modules', () => {
const resolver = createRequireResolver(defaultOpts);
expect(() => resolver('lodash')).not.toThrow();
});
it('should throw when requiring non-whitelisted external modules', () => {
const resolver = createRequireResolver(defaultOpts);
expect(() => resolver('express')).toThrow(
new ExecutionError(new ApplicationError("Cannot find module 'express'")),
);
});
it('should allow all external modules when allowedExternalModules is "*"', () => {
const resolver = createRequireResolver({
...defaultOpts,
allowedExternalModules: '*',
});
expect(() => resolver('lodash')).not.toThrow();
expect(() => resolver('express')).not.toThrow();
});
});
describe('error handling', () => {
it('should wrap ApplicationError in ExecutionError', () => {
const resolver = createRequireResolver(defaultOpts);
expect(() => resolver('non-existent-module')).toThrow(ExecutionError);
});
it('should include the module name in the error message', () => {
const resolver = createRequireResolver(defaultOpts);
expect(() => resolver('non-existent-module')).toThrow(
"Cannot find module 'non-existent-module'",
);
});
});
});

View file

@ -98,17 +98,33 @@ export class JsTaskRunner extends TaskRunner {
const { jsRunnerConfig } = config; const { jsRunnerConfig } = config;
const parseModuleAllowList = (moduleList: string) => const parseModuleAllowList = (moduleList: string) =>
moduleList === '*' ? null : new Set(moduleList.split(',').map((x) => x.trim())); moduleList === '*' ? '*' : new Set(moduleList.split(',').map((x) => x.trim()));
const allowedBuiltInModules = parseModuleAllowList(jsRunnerConfig.allowedBuiltInModules ?? '');
const allowedExternalModules = parseModuleAllowList(
jsRunnerConfig.allowedExternalModules ?? '',
);
this.requireResolver = createRequireResolver({ this.requireResolver = createRequireResolver({
allowedBuiltInModules: parseModuleAllowList(jsRunnerConfig.allowedBuiltInModules ?? ''), allowedBuiltInModules,
allowedExternalModules: parseModuleAllowList(jsRunnerConfig.allowedExternalModules ?? ''), allowedExternalModules,
}); });
this.preventPrototypePollution(); this.preventPrototypePollution(allowedExternalModules);
}
private preventPrototypePollution(allowedExternalModules: Set<string> | '*') {
if (allowedExternalModules instanceof Set) {
// This is a workaround to enable the allowed external libraries to mutate
// prototypes directly. For example momentjs overrides .toString() directly
// on the Moment.prototype, which doesn't work if Object.prototype has been
// frozen. This works as long as the overrides are done when the library is
// imported.
for (const module of allowedExternalModules) {
require(module);
}
} }
private preventPrototypePollution() {
// Freeze globals, except for Jest // Freeze globals, except for Jest
if (process.env.NODE_ENV !== 'test') { if (process.env.NODE_ENV !== 'test') {
Object.getOwnPropertyNames(globalThis) Object.getOwnPropertyNames(globalThis)

View file

@ -6,15 +6,15 @@ import { ExecutionError } from './errors/execution-error';
export type RequireResolverOpts = { export type RequireResolverOpts = {
/** /**
* List of built-in nodejs modules that are allowed to be required in the * List of built-in nodejs modules that are allowed to be required in the
* execution sandbox. `null` means all are allowed. * execution sandbox. `"*"` means all are allowed.
*/ */
allowedBuiltInModules: Set<string> | null; allowedBuiltInModules: Set<string> | '*';
/** /**
* List of external modules that are allowed to be required in the * List of external modules that are allowed to be required in the
* execution sandbox. `null` means all are allowed. * execution sandbox. `"*"` means all are allowed.
*/ */
allowedExternalModules: Set<string> | null; allowedExternalModules: Set<string> | '*';
}; };
export type RequireResolver = (request: string) => unknown; export type RequireResolver = (request: string) => unknown;
@ -24,8 +24,8 @@ export function createRequireResolver({
allowedExternalModules, allowedExternalModules,
}: RequireResolverOpts) { }: RequireResolverOpts) {
return (request: string) => { return (request: string) => {
const checkIsAllowed = (allowList: Set<string> | null, moduleName: string) => { const checkIsAllowed = (allowList: Set<string> | '*', moduleName: string) => {
return allowList ? allowList.has(moduleName) : true; return allowList === '*' || allowList.has(moduleName);
}; };
const isAllowed = isBuiltin(request) const isAllowed = isBuiltin(request)