mirror of
https://github.com/n8n-io/n8n.git
synced 2025-02-02 07:01:30 -08:00
fix(core): Fix usage of external libs in task runner (#12788)
Co-authored-by: Cornelius Suermann <cornelius@n8n.io>
This commit is contained in:
parent
fb662dd95c
commit
3d9d5bf9d5
|
@ -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'",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -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() {
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in a new issue