diff --git a/packages/@n8n/api-types/src/frontend-settings.ts b/packages/@n8n/api-types/src/frontend-settings.ts index 1a4f1cfc17..ab436703da 100644 --- a/packages/@n8n/api-types/src/frontend-settings.ts +++ b/packages/@n8n/api-types/src/frontend-settings.ts @@ -1,4 +1,4 @@ -import type { ExpressionEvaluatorType, LogLevel, WorkflowSettings } from 'n8n-workflow'; +import type { LogLevel, WorkflowSettings } from 'n8n-workflow'; import { type InsightsDateRange } from './schemas/insights.schema'; @@ -157,9 +157,6 @@ export interface FrontendSettings { variables: { limit: number; }; - expressions: { - evaluator: ExpressionEvaluatorType; - }; mfa: { enabled: boolean; }; diff --git a/packages/cli/src/commands/base-command.ts b/packages/cli/src/commands/base-command.ts index d15f4a44d8..639456df39 100644 --- a/packages/cli/src/commands/base-command.ts +++ b/packages/cli/src/commands/base-command.ts @@ -25,7 +25,6 @@ import { DeprecationService } from '@/deprecation/deprecation.service'; import { TestRunnerService } from '@/evaluation.ee/test-runner/test-runner.service.ee'; import { MessageEventBus } from '@/eventbus/message-event-bus/message-event-bus'; import { TelemetryEventRelay } from '@/events/relays/telemetry.event-relay'; -import { initExpressionEvaluator } from '@/expression-evaluator'; import { ExternalHooks } from '@/external-hooks'; import { ExternalSecretsManager } from '@/external-secrets.ee/external-secrets-manager.ee'; import { License } from '@/license'; @@ -116,7 +115,6 @@ export abstract class BaseCommand extends Command { serverName: deploymentName, releaseDate: N8N_RELEASE_DATE, }); - initExpressionEvaluator(); process.once('SIGTERM', this.onTerminationSignal('SIGTERM')); process.once('SIGINT', this.onTerminationSignal('SIGINT')); diff --git a/packages/cli/src/config/schema.ts b/packages/cli/src/config/schema.ts index 5905741cbb..6d7fde9259 100644 --- a/packages/cli/src/config/schema.ts +++ b/packages/cli/src/config/schema.ts @@ -279,21 +279,6 @@ export const schema = { }, }, - expression: { - evaluator: { - doc: 'Expression evaluator to use', - format: ['tmpl', 'tournament'] as const, - default: 'tournament', - env: 'N8N_EXPRESSION_EVALUATOR', - }, - reportDifference: { - doc: 'Whether to report differences in the evaluator outputs', - format: Boolean, - default: false, - env: 'N8N_EXPRESSION_REPORT_DIFFERENCE', - }, - }, - proxy_hops: { format: Number, default: 0, diff --git a/packages/cli/src/deprecation/deprecation.service.ts b/packages/cli/src/deprecation/deprecation.service.ts index 59cb8ee813..c4b2d5bb19 100644 --- a/packages/cli/src/deprecation/deprecation.service.ts +++ b/packages/cli/src/deprecation/deprecation.service.ts @@ -76,6 +76,14 @@ export class DeprecationService { envVar: 'N8N_PARTIAL_EXECUTION_VERSION_DEFAULT', message: 'This environment variable is internal and should not be set.', }, + { + envVar: 'N8N_EXPRESSION_EVALUATOR', + message: `n8n has replaced \`tmpl\` with \`tournament\` as expression evaluator. ${SAFE_TO_REMOVE}`, + }, + { + envVar: 'N8N_EXPRESSION_REPORT_DIFFERENCE', + message: `n8n has replaced \`tmpl\` with \`tournament\` as expression evaluator. ${SAFE_TO_REMOVE}`, + }, { envVar: 'EXECUTIONS_PROCESS', message: SAFE_TO_REMOVE, diff --git a/packages/cli/src/expression-evaluator.ts b/packages/cli/src/expression-evaluator.ts deleted file mode 100644 index d77838dbc1..0000000000 --- a/packages/cli/src/expression-evaluator.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Container } from '@n8n/di'; -import { ErrorReporter } from 'n8n-core'; -import { ExpressionEvaluatorProxy } from 'n8n-workflow'; - -import config from '@/config'; - -export const initExpressionEvaluator = () => { - ExpressionEvaluatorProxy.setEvaluator(config.getEnv('expression.evaluator')); - ExpressionEvaluatorProxy.setDifferEnabled(config.getEnv('expression.reportDifference')); - ExpressionEvaluatorProxy.setDiffReporter((expr) => { - Container.get(ErrorReporter).warn('Expression difference', { - extra: { - expression: expr, - }, - }); - }); -}; diff --git a/packages/cli/src/services/frontend.service.ts b/packages/cli/src/services/frontend.service.ts index be64b7dcf3..6dc2dc2133 100644 --- a/packages/cli/src/services/frontend.service.ts +++ b/packages/cli/src/services/frontend.service.ts @@ -220,9 +220,6 @@ export class FrontendService { variables: { limit: 0, }, - expressions: { - evaluator: config.getEnv('expression.evaluator'), - }, banners: { dismissed: [], }, diff --git a/packages/core/nodes-testing/node-test-harness.ts b/packages/core/nodes-testing/node-test-harness.ts index 5d1ccf2766..43100c2e7c 100644 --- a/packages/core/nodes-testing/node-test-harness.ts +++ b/packages/core/nodes-testing/node-test-harness.ts @@ -12,12 +12,7 @@ import type { IWorkflowExecuteAdditionalData, WorkflowTestData, } from 'n8n-workflow'; -import { - createDeferredPromise, - ExpressionEvaluatorProxy, - UnexpectedError, - Workflow, -} from 'n8n-workflow'; +import { createDeferredPromise, UnexpectedError, Workflow } from 'n8n-workflow'; import nock from 'nock'; import { readFileSync, mkdtempSync, existsSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; @@ -49,7 +44,6 @@ export class NodeTestHarness { private readonly packagePaths: string[]; constructor({ additionalPackagePaths }: TestHarnessOptions = {}) { - ExpressionEvaluatorProxy.setEvaluator('tournament'); this.testDir = path.dirname(callsites()[1].getFileName()!); this.packagePaths = additionalPackagePaths ?? []; this.packagePaths.unshift(this.packageDir); diff --git a/packages/frontend/editor-ui/src/__tests__/defaults.ts b/packages/frontend/editor-ui/src/__tests__/defaults.ts index f3af21136d..707790f5eb 100644 --- a/packages/frontend/editor-ui/src/__tests__/defaults.ts +++ b/packages/frontend/editor-ui/src/__tests__/defaults.ts @@ -44,9 +44,6 @@ export const defaultSettings: FrontendSettings = { }, }, }, - expressions: { - evaluator: 'tournament', - }, executionMode: 'regular', isMultiMain: false, executionTimeout: 0, diff --git a/packages/frontend/editor-ui/src/composables/useNodeHelpers.ts b/packages/frontend/editor-ui/src/composables/useNodeHelpers.ts index a9f9bcb867..02140cb761 100644 --- a/packages/frontend/editor-ui/src/composables/useNodeHelpers.ts +++ b/packages/frontend/editor-ui/src/composables/useNodeHelpers.ts @@ -2,7 +2,7 @@ import { ref } from 'vue'; import { useHistoryStore } from '@/stores/history.store'; import { CUSTOM_API_CALL_KEY, PLACEHOLDER_FILLED_AT_EXECUTION_TIME } from '@/constants'; -import { NodeHelpers, ExpressionEvaluatorProxy, NodeConnectionTypes } from 'n8n-workflow'; +import { NodeHelpers, NodeConnectionTypes } from 'n8n-workflow'; import type { INodeProperties, INodeCredentialDescription, @@ -37,7 +37,6 @@ import type { import { isString } from '@/utils/typeGuards'; import { isObject } from '@/utils/objectUtils'; -import { useSettingsStore } from '@/stores/settings.store'; import { useWorkflowsStore } from '@/stores/workflows.store'; import { useNodeTypesStore } from '@/stores/nodeTypes.store'; import { useCredentialsStore } from '@/stores/credentials.store'; @@ -702,9 +701,6 @@ export function useNodeHelpers() { if (nodeType?.subtitle !== undefined) { try { - ExpressionEvaluatorProxy.setEvaluator( - useSettingsStore().settings.expressions?.evaluator ?? 'tmpl', - ); return workflow.expression.getSimpleParameterValue( data, nodeType.subtitle, diff --git a/packages/frontend/editor-ui/src/composables/useWorkflowHelpers.ts b/packages/frontend/editor-ui/src/composables/useWorkflowHelpers.ts index e4de532599..acd49b58cd 100644 --- a/packages/frontend/editor-ui/src/composables/useWorkflowHelpers.ts +++ b/packages/frontend/editor-ui/src/composables/useWorkflowHelpers.ts @@ -24,12 +24,7 @@ import type { NodeParameterValue, Workflow, } from 'n8n-workflow'; -import { - NodeConnectionTypes, - ExpressionEvaluatorProxy, - NodeHelpers, - WEBHOOK_NODE_TYPE, -} from 'n8n-workflow'; +import { NodeConnectionTypes, NodeHelpers, WEBHOOK_NODE_TYPE } from 'n8n-workflow'; import type { ICredentialsResponse, @@ -60,7 +55,6 @@ import { useTemplatesStore } from '@/stores/templates.store'; import { useUIStore } from '@/stores/ui.store'; import { useWorkflowsStore } from '@/stores/workflows.store'; import { getSourceItems } from '@/utils/pairedItemUtils'; -import { useSettingsStore } from '@/stores/settings.store'; import { getCredentialTypeName, isCredentialOnlyNodeType } from '@/utils/credentialOnlyNodes'; import { useDocumentTitle } from '@/composables/useDocumentTitle'; import { useExternalHooks } from '@/composables/useExternalHooks'; @@ -229,9 +223,6 @@ export function resolveParameter( _executeData = executeData(parentNode, contextNode!.name, inputName, runIndexParent); } - ExpressionEvaluatorProxy.setEvaluator( - useSettingsStore().settings.expressions?.evaluator ?? 'tmpl', - ); return workflow.expression.getParameterValue( parameter, runExecutionData, diff --git a/packages/frontend/editor-ui/src/stores/settings.store.ts b/packages/frontend/editor-ui/src/stores/settings.store.ts index 8a9ec36cfd..05c488338a 100644 --- a/packages/frontend/editor-ui/src/stores/settings.store.ts +++ b/packages/frontend/editor-ui/src/stores/settings.store.ts @@ -10,7 +10,6 @@ import type { ILdapConfig } from '@/Interface'; import { STORES, INSECURE_CONNECTION_WARNING } from '@/constants'; import { UserManagementAuthenticationMethod } from '@/Interface'; import type { IDataObject, WorkflowSettings } from 'n8n-workflow'; -import { ExpressionEvaluatorProxy } from 'n8n-workflow'; import { defineStore } from 'pinia'; import { useRootStore } from './root.store'; import { useUIStore } from './ui.store'; @@ -300,8 +299,6 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, () => { try { await getSettings(); - ExpressionEvaluatorProxy.setEvaluator(settings.value.expressions.evaluator); - initialized.value = true; } catch (e) { showToast({ diff --git a/packages/workflow/package.json b/packages/workflow/package.json index ffd6dd0ce6..7151c221eb 100644 --- a/packages/workflow/package.json +++ b/packages/workflow/package.json @@ -43,7 +43,6 @@ }, "dependencies": { "@n8n/tournament": "1.0.6", - "@n8n_io/riot-tmpl": "4.0.0", "ast-types": "0.15.2", "axios": "catalog:", "callsites": "catalog:", diff --git a/packages/workflow/src/Expression.ts b/packages/workflow/src/Expression.ts index 1c956f0b5b..98f2873e9a 100644 --- a/packages/workflow/src/Expression.ts +++ b/packages/workflow/src/Expression.ts @@ -1,4 +1,3 @@ -import * as tmpl from '@n8n_io/riot-tmpl'; import { DateTime, Duration, Interval } from 'luxon'; import { ApplicationError } from './errors/application.error'; @@ -53,7 +52,7 @@ export class Expression { constructor(private readonly workflow: Workflow) {} static resolveWithoutWorkflow(expression: string, data: IDataObject = {}) { - return tmpl.tmpl(expression, data); + return evaluateExpression(expression, data); } /** @@ -327,10 +326,7 @@ export class Expression { return returnValue; } - private renderExpression( - expression: string, - data: IWorkflowDataProxyData, - ): tmpl.ReturnValue | undefined { + private renderExpression(expression: string, data: IWorkflowDataProxyData) { try { return evaluateExpression(expression, data); } catch (error) { diff --git a/packages/workflow/src/ExpressionEvaluatorProxy.ts b/packages/workflow/src/ExpressionEvaluatorProxy.ts index 37dec8af51..6525b343a2 100644 --- a/packages/workflow/src/ExpressionEvaluatorProxy.ts +++ b/packages/workflow/src/ExpressionEvaluatorProxy.ts @@ -1,155 +1,21 @@ -import type { ReturnValue, TmplDifference } from '@n8n/tournament'; import { Tournament } from '@n8n/tournament'; -import * as tmpl from '@n8n_io/riot-tmpl'; import { PrototypeSanitizer } from './ExpressionSandboxing'; -import type { ExpressionEvaluatorType } from './Interfaces'; -import * as LoggerProxy from './LoggerProxy'; -type Evaluator = (expr: string, data: unknown) => tmpl.ReturnValue; +type Evaluator = (expr: string, data: unknown) => string | null | (() => unknown); type ErrorHandler = (error: Error) => void; -type DifferenceHandler = (expr: string) => void; -// Set it to use double curly brackets instead of single ones -tmpl.brackets.set('{{ }}'); - -let errorHandler: ErrorHandler = () => {}; -let differenceHandler: DifferenceHandler = () => {}; -const differenceChecker = (diff: TmplDifference) => { - try { - if (diff.same) { - return; - } - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - if (diff.has?.function || diff.has?.templateString) { - return; - } - if (diff.expression === 'UNPARSEABLE') { - differenceHandler(diff.expression); - } else { - differenceHandler(diff.expression.value); - } - } catch { - LoggerProxy.error('Expression evaluator difference checker failed'); - } -}; +const errorHandler: ErrorHandler = () => {}; const tournamentEvaluator = new Tournament(errorHandler, undefined, undefined, { before: [], after: [PrototypeSanitizer], }); -let evaluator: Evaluator = tmpl.tmpl; -let currentEvaluatorType: ExpressionEvaluatorType = 'tmpl'; -let diffExpressions = false; +const evaluator: Evaluator = tournamentEvaluator.execute.bind(tournamentEvaluator); export const setErrorHandler = (handler: ErrorHandler) => { - errorHandler = handler; - tmpl.tmpl.errorHandler = handler; tournamentEvaluator.errorHandler = handler; }; -export const setEvaluator = (evalType: ExpressionEvaluatorType) => { - currentEvaluatorType = evalType; - if (evalType === 'tmpl') { - evaluator = tmpl.tmpl; - } else if (evalType === 'tournament') { - evaluator = tournamentEvaluator.execute.bind(tournamentEvaluator); - } -}; - -export const setDiffReporter = (reporter: (expr: string) => void) => { - differenceHandler = reporter; -}; - -export const setDifferEnabled = (enabled: boolean) => { - diffExpressions = enabled; -}; - -const diffCache: Record = {}; - -export const checkEvaluatorDifferences = (expr: string): TmplDifference | null => { - if (expr in diffCache) { - return diffCache[expr]; - } - let diff: TmplDifference | null; - try { - diff = tournamentEvaluator.tmplDiff(expr); - } catch { - // We don't include the expression for privacy reasons - try { - differenceHandler('ERROR'); - } catch {} - diff = null; - } - - if (diff?.same === false) { - differenceChecker(diff); - } - - diffCache[expr] = diff; - return diff; -}; - -export const getEvaluator = () => { - return evaluator; -}; - export const evaluateExpression: Evaluator = (expr, data) => { - if (!diffExpressions) { - return evaluator(expr, data); - } - const diff = checkEvaluatorDifferences(expr); - - // We already know that they're different so don't bother - // evaluating with both evaluators - if (!diff?.same) { - return evaluator(expr, data); - } - - let tmplValue: tmpl.ReturnValue; - let tournValue: ReturnValue; - let wasTmplError = false; - let tmplError: unknown; - let wasTournError = false; - let tournError: unknown; - - try { - tmplValue = tmpl.tmpl(expr, data); - } catch (error) { - tmplError = error; - wasTmplError = true; - } - - try { - tournValue = tournamentEvaluator.execute(expr, data); - } catch (error) { - tournError = error; - wasTournError = true; - } - - if ( - wasTmplError !== wasTournError || - JSON.stringify(tmplValue!) !== JSON.stringify(tournValue!) - ) { - try { - if (diff.expression) { - differenceHandler(diff.expression.value); - } else { - differenceHandler('VALUEDIFF'); - } - } catch { - LoggerProxy.error('Failed to report error difference'); - } - } - - if (currentEvaluatorType === 'tmpl') { - if (wasTmplError) { - throw tmplError; - } - return tmplValue!; - } - - if (wasTournError) { - throw tournError; - } - return tournValue!; + return evaluator(expr, data); }; diff --git a/packages/workflow/src/Extensions/ExpressionExtension.ts b/packages/workflow/src/Extensions/ExpressionExtension.ts index 1f3b8d2fc0..81efe99ea7 100644 --- a/packages/workflow/src/Extensions/ExpressionExtension.ts +++ b/packages/workflow/src/Extensions/ExpressionExtension.ts @@ -134,7 +134,7 @@ export const extendTransform = (expression: string): { code: string } | undefine const chainNumber = currentChain; currentChain += 1; - // This is to match our fork of tmpl + // This is to match behavior in our original expression evaluator (tmpl) const globalIdentifier = types.builders.identifier( // eslint-disable-next-line @typescript-eslint/ban-ts-comment typeof window !== 'object' ? 'global' : 'window', diff --git a/packages/workflow/src/Extensions/ExpressionParser.ts b/packages/workflow/src/Extensions/ExpressionParser.ts index 3e94bfb2eb..004211edc3 100644 --- a/packages/workflow/src/Extensions/ExpressionParser.ts +++ b/packages/workflow/src/Extensions/ExpressionParser.ts @@ -6,8 +6,9 @@ export interface ExpressionText { export interface ExpressionCode { type: 'code'; text: string; - // tmpl has different behaviours if the last expression - // doesn't close itself. + + // This is to match behavior in our original expression evaluator (tmpl), + // which has different behaviours if the last expression doesn't close itself. hasClosingBrackets: boolean; } diff --git a/packages/workflow/src/Interfaces.ts b/packages/workflow/src/Interfaces.ts index ef894dd798..505896bc64 100644 --- a/packages/workflow/src/Interfaces.ts +++ b/packages/workflow/src/Interfaces.ts @@ -2872,8 +2872,6 @@ export interface ICheckProcessedContextData { }; } -export type ExpressionEvaluatorType = 'tmpl' | 'tournament'; - export type N8nAIProviderType = 'openai' | 'unknown'; export interface SecretsHelpersBase { diff --git a/packages/workflow/src/index.ts b/packages/workflow/src/index.ts index 4e13469fa9..0ed7a2c93e 100644 --- a/packages/workflow/src/index.ts +++ b/packages/workflow/src/index.ts @@ -1,5 +1,4 @@ import * as LoggerProxy from './LoggerProxy'; -export * as ExpressionEvaluatorProxy from './ExpressionEvaluatorProxy'; import * as NodeHelpers from './NodeHelpers'; import * as ObservableObject from './ObservableObject'; import * as TelemetryHelpers from './TelemetryHelpers'; diff --git a/packages/workflow/test/Expression.test.ts b/packages/workflow/test/Expression.test.ts index 22378a11a4..a365a733b9 100644 --- a/packages/workflow/test/Expression.test.ts +++ b/packages/workflow/test/Expression.test.ts @@ -5,7 +5,6 @@ import { DateTime, Duration, Interval } from 'luxon'; import { ExpressionError } from '@/errors/expression.error'; -import { setDifferEnabled, setEvaluator } from '@/ExpressionEvaluatorProxy'; import { extendSyntax } from '@/Extensions/ExpressionExtension'; import type { INodeExecutionData } from '@/Interfaces'; import { Workflow } from '@/Workflow'; @@ -15,202 +14,206 @@ import { baseFixtures } from './ExpressionFixtures/base'; import type { ExpressionTestEvaluation, ExpressionTestTransform } from './ExpressionFixtures/base'; import * as Helpers from './Helpers'; -setDifferEnabled(true); +describe('Expression', () => { + describe('getParameterValue()', () => { + const nodeTypes = Helpers.NodeTypes(); + const workflow = new Workflow({ + id: '1', + nodes: [ + { + name: 'node', + typeVersion: 1, + type: 'test.set', + id: 'uuid-1234', + position: [0, 0], + parameters: {}, + }, + ], + connections: {}, + active: false, + nodeTypes, + }); + const expression = workflow.expression; -for (const evaluator of ['tmpl', 'tournament'] as const) { - setEvaluator(evaluator); - describe(`Expression (with ${evaluator})`, () => { - describe('getParameterValue()', () => { - const nodeTypes = Helpers.NodeTypes(); - const workflow = new Workflow({ - id: '1', - nodes: [ - { - name: 'node', - typeVersion: 1, - type: 'test.set', - id: 'uuid-1234', - position: [0, 0], - parameters: {}, - }, - ], - connections: {}, - active: false, - nodeTypes, - }); - const expression = workflow.expression; + const evaluate = (value: string) => + expression.getParameterValue(value, null, 0, 0, 'node', [], 'manual', {}); - const evaluate = (value: string) => - expression.getParameterValue(value, null, 0, 0, 'node', [], 'manual', {}); + it('should not be able to use global built-ins from denylist', () => { + expect(evaluate('={{document}}')).toEqual({}); + expect(evaluate('={{window}}')).toEqual({}); - it('should not be able to use global built-ins from denylist', () => { - expect(evaluate('={{document}}')).toEqual({}); - expect(evaluate('={{window}}')).toEqual({}); + expect(evaluate('={{Window}}')).toEqual({}); + expect(evaluate('={{globalThis}}')).toEqual({}); + expect(evaluate('={{self}}')).toEqual({}); - expect(evaluate('={{Window}}')).toEqual({}); - expect(evaluate('={{globalThis}}')).toEqual({}); - expect(evaluate('={{self}}')).toEqual({}); + expect(evaluate('={{alert}}')).toEqual({}); + expect(evaluate('={{prompt}}')).toEqual({}); + expect(evaluate('={{confirm}}')).toEqual({}); - expect(evaluate('={{alert}}')).toEqual({}); - expect(evaluate('={{prompt}}')).toEqual({}); - expect(evaluate('={{confirm}}')).toEqual({}); + expect(evaluate('={{eval}}')).toEqual({}); + expect(evaluate('={{uneval}}')).toEqual({}); + expect(evaluate('={{setTimeout}}')).toEqual({}); + expect(evaluate('={{setInterval}}')).toEqual({}); + expect(evaluate('={{Function}}')).toEqual({}); - expect(evaluate('={{eval}}')).toEqual({}); - expect(evaluate('={{uneval}}')).toEqual({}); - expect(evaluate('={{setTimeout}}')).toEqual({}); - expect(evaluate('={{setInterval}}')).toEqual({}); - expect(evaluate('={{Function}}')).toEqual({}); + expect(evaluate('={{fetch}}')).toEqual({}); + expect(evaluate('={{XMLHttpRequest}}')).toEqual({}); - expect(evaluate('={{fetch}}')).toEqual({}); - expect(evaluate('={{XMLHttpRequest}}')).toEqual({}); + expect(evaluate('={{Promise}}')).toEqual({}); + expect(evaluate('={{Generator}}')).toEqual({}); + expect(evaluate('={{GeneratorFunction}}')).toEqual({}); + expect(evaluate('={{AsyncFunction}}')).toEqual({}); + expect(evaluate('={{AsyncGenerator}}')).toEqual({}); + expect(evaluate('={{AsyncGeneratorFunction}}')).toEqual({}); - expect(evaluate('={{Promise}}')).toEqual({}); - expect(evaluate('={{Generator}}')).toEqual({}); - expect(evaluate('={{GeneratorFunction}}')).toEqual({}); - expect(evaluate('={{AsyncFunction}}')).toEqual({}); - expect(evaluate('={{AsyncGenerator}}')).toEqual({}); - expect(evaluate('={{AsyncGeneratorFunction}}')).toEqual({}); + expect(evaluate('={{WebAssembly}}')).toEqual({}); - expect(evaluate('={{WebAssembly}}')).toEqual({}); + expect(evaluate('={{Reflect}}')).toEqual({}); + expect(evaluate('={{Proxy}}')).toEqual({}); - expect(evaluate('={{Reflect}}')).toEqual({}); - expect(evaluate('={{Proxy}}')).toEqual({}); + expect(() => evaluate('={{constructor}}')).toThrowError( + new ExpressionError('Cannot access "constructor" due to security concerns'), + ); - expect(() => evaluate('={{constructor}}')).toThrowError( - new ExpressionError('Cannot access "constructor" due to security concerns'), - ); - - expect(evaluate('={{escape}}')).toEqual({}); - expect(evaluate('={{unescape}}')).toEqual({}); - }); - - it('should be able to use global built-ins from allowlist', () => { - expect(evaluate('={{new Date()}}')).toBeInstanceOf(Date); - expect(evaluate('={{DateTime.now().toLocaleString()}}')).toEqual( - DateTime.now().toLocaleString(), - ); - - jest.useFakeTimers({ now: new Date() }); - expect(evaluate('={{Interval.after(new Date(), 100)}}')).toEqual( - Interval.after(new Date(), 100), - ); - jest.useRealTimers(); - - expect(evaluate('={{Duration.fromMillis(100)}}')).toEqual(Duration.fromMillis(100)); - - expect(evaluate('={{new Object()}}')).toEqual(new Object()); - - expect(evaluate('={{new Array()}}')).toEqual([]); - expect(evaluate('={{new Int8Array()}}')).toEqual(new Int8Array()); - expect(evaluate('={{new Uint8Array()}}')).toEqual(new Uint8Array()); - expect(evaluate('={{new Uint8ClampedArray()}}')).toEqual(new Uint8ClampedArray()); - expect(evaluate('={{new Int16Array()}}')).toEqual(new Int16Array()); - expect(evaluate('={{new Uint16Array()}}')).toEqual(new Uint16Array()); - expect(evaluate('={{new Int32Array()}}')).toEqual(new Int32Array()); - expect(evaluate('={{new Uint32Array()}}')).toEqual(new Uint32Array()); - expect(evaluate('={{new Float32Array()}}')).toEqual(new Float32Array()); - expect(evaluate('={{new Float64Array()}}')).toEqual(new Float64Array()); - expect(evaluate('={{new BigInt64Array()}}')).toEqual(new BigInt64Array()); - expect(evaluate('={{new BigUint64Array()}}')).toEqual(new BigUint64Array()); - - expect(evaluate('={{new Map()}}')).toEqual(new Map()); - expect(evaluate('={{new WeakMap()}}')).toEqual(new WeakMap()); - expect(evaluate('={{new Set()}}')).toEqual(new Set()); - expect(evaluate('={{new WeakSet()}}')).toEqual(new WeakSet()); - - expect(evaluate('={{new Error()}}')).toEqual(new Error()); - expect(evaluate('={{new TypeError()}}')).toEqual(new TypeError()); - expect(evaluate('={{new SyntaxError()}}')).toEqual(new SyntaxError()); - expect(evaluate('={{new EvalError()}}')).toEqual(new EvalError()); - expect(evaluate('={{new RangeError()}}')).toEqual(new RangeError()); - expect(evaluate('={{new ReferenceError()}}')).toEqual(new ReferenceError()); - expect(evaluate('={{new URIError()}}')).toEqual(new URIError()); - - expect(evaluate('={{Intl}}')).toEqual(Intl); - - expect(evaluate('={{new String()}}')).toEqual(new String()); - expect(evaluate("={{new RegExp('')}}")).toEqual(new RegExp('')); - - expect(evaluate('={{Math}}')).toEqual(Math); - expect(evaluate('={{new Number()}}')).toEqual(new Number()); - expect(evaluate("={{BigInt('1')}}")).toEqual(BigInt('1')); - expect(evaluate('={{Infinity}}')).toEqual(Infinity); - expect(evaluate('={{NaN}}')).toEqual(NaN); - expect(evaluate('={{isFinite(1)}}')).toEqual(isFinite(1)); - expect(evaluate('={{isNaN(1)}}')).toEqual(isNaN(1)); - expect(evaluate("={{parseFloat('1')}}")).toEqual(parseFloat('1')); - expect(evaluate("={{parseInt('1', 10)}}")).toEqual(parseInt('1', 10)); - - expect(evaluate('={{JSON.stringify({})}}')).toEqual(JSON.stringify({})); - expect(evaluate('={{new ArrayBuffer(10)}}')).toEqual(new ArrayBuffer(10)); - expect(evaluate('={{new SharedArrayBuffer(10)}}')).toEqual(new SharedArrayBuffer(10)); - expect(evaluate('={{Atomics}}')).toEqual(Atomics); - expect(evaluate('={{new DataView(new ArrayBuffer(1))}}')).toEqual( - new DataView(new ArrayBuffer(1)), - ); - - expect(evaluate("={{encodeURI('https://google.com')}}")).toEqual( - encodeURI('https://google.com'), - ); - expect(evaluate("={{encodeURIComponent('https://google.com')}}")).toEqual( - encodeURIComponent('https://google.com'), - ); - expect(evaluate("={{decodeURI('https://google.com')}}")).toEqual( - decodeURI('https://google.com'), - ); - expect(evaluate("={{decodeURIComponent('https://google.com')}}")).toEqual( - decodeURIComponent('https://google.com'), - ); - - expect(evaluate('={{Boolean(1)}}')).toEqual(Boolean(1)); - expect(evaluate('={{Symbol(1).toString()}}')).toEqual(Symbol(1).toString()); - }); + expect(evaluate('={{escape}}')).toEqual({}); + expect(evaluate('={{unescape}}')).toEqual({}); }); - describe('Test all expression value fixtures', () => { - const expression = workflow.expression; + it('should be able to use global built-ins from allowlist', () => { + expect(evaluate('={{new Date()}}')).toBeInstanceOf(Date); + expect(evaluate('={{DateTime.now().toLocaleString()}}')).toEqual( + DateTime.now().toLocaleString(), + ); - const evaluate = (value: string, data: INodeExecutionData[]) => { - const itemIndex = data.length === 0 ? -1 : 0; - return expression.getParameterValue(value, null, 0, itemIndex, 'node', data, 'manual', {}); - }; + jest.useFakeTimers({ now: new Date() }); + expect(evaluate('={{Interval.after(new Date(), 100)}}')).toEqual( + Interval.after(new Date(), 100), + ); + jest.useRealTimers(); - for (const t of baseFixtures) { - if (!t.tests.some((test) => test.type === 'evaluation')) { - continue; - } - test(t.expression, () => { - const evaluationTests = t.tests.filter( - (test): test is ExpressionTestEvaluation => test.type === 'evaluation', - ); + expect(evaluate('={{Duration.fromMillis(100)}}')).toEqual(Duration.fromMillis(100)); - for (const test of evaluationTests) { - const input = test.input.map((d) => ({ json: d })) as any; + expect(evaluate('={{new Object()}}')).toEqual(new Object()); - if ('error' in test) { - expect(() => evaluate(t.expression, input)).toThrowError(test.error); - } else { - expect(evaluate(t.expression, input)).toStrictEqual(test.output); - } - } - }); - } + expect(evaluate('={{new Array()}}')).toEqual([]); + expect(evaluate('={{new Int8Array()}}')).toEqual(new Int8Array()); + expect(evaluate('={{new Uint8Array()}}')).toEqual(new Uint8Array()); + expect(evaluate('={{new Uint8ClampedArray()}}')).toEqual(new Uint8ClampedArray()); + expect(evaluate('={{new Int16Array()}}')).toEqual(new Int16Array()); + expect(evaluate('={{new Uint16Array()}}')).toEqual(new Uint16Array()); + expect(evaluate('={{new Int32Array()}}')).toEqual(new Int32Array()); + expect(evaluate('={{new Uint32Array()}}')).toEqual(new Uint32Array()); + expect(evaluate('={{new Float32Array()}}')).toEqual(new Float32Array()); + expect(evaluate('={{new Float64Array()}}')).toEqual(new Float64Array()); + expect(evaluate('={{new BigInt64Array()}}')).toEqual(new BigInt64Array()); + expect(evaluate('={{new BigUint64Array()}}')).toEqual(new BigUint64Array()); + + expect(evaluate('={{new Map()}}')).toEqual(new Map()); + expect(evaluate('={{new WeakMap()}}')).toEqual(new WeakMap()); + expect(evaluate('={{new Set()}}')).toEqual(new Set()); + expect(evaluate('={{new WeakSet()}}')).toEqual(new WeakSet()); + + expect(evaluate('={{new Error()}}')).toEqual(new Error()); + expect(evaluate('={{new TypeError()}}')).toEqual(new TypeError()); + expect(evaluate('={{new SyntaxError()}}')).toEqual(new SyntaxError()); + expect(evaluate('={{new EvalError()}}')).toEqual(new EvalError()); + expect(evaluate('={{new RangeError()}}')).toEqual(new RangeError()); + expect(evaluate('={{new ReferenceError()}}')).toEqual(new ReferenceError()); + expect(evaluate('={{new URIError()}}')).toEqual(new URIError()); + + expect(evaluate('={{Intl}}')).toEqual(Intl); + + expect(evaluate('={{new String()}}')).toEqual(new String()); + expect(evaluate("={{new RegExp('')}}")).toEqual(new RegExp('')); + + expect(evaluate('={{Math}}')).toEqual(Math); + expect(evaluate('={{new Number()}}')).toEqual(new Number()); + expect(evaluate("={{BigInt('1')}}")).toEqual(BigInt('1')); + expect(evaluate('={{Infinity}}')).toEqual(Infinity); + expect(evaluate('={{NaN}}')).toEqual(NaN); + expect(evaluate('={{isFinite(1)}}')).toEqual(isFinite(1)); + expect(evaluate('={{isNaN(1)}}')).toEqual(isNaN(1)); + expect(evaluate("={{parseFloat('1')}}")).toEqual(parseFloat('1')); + expect(evaluate("={{parseInt('1', 10)}}")).toEqual(parseInt('1', 10)); + + expect(evaluate('={{JSON.stringify({})}}')).toEqual(JSON.stringify({})); + expect(evaluate('={{new ArrayBuffer(10)}}')).toEqual(new ArrayBuffer(10)); + expect(evaluate('={{new SharedArrayBuffer(10)}}')).toEqual(new SharedArrayBuffer(10)); + expect(evaluate('={{Atomics}}')).toEqual(Atomics); + expect(evaluate('={{new DataView(new ArrayBuffer(1))}}')).toEqual( + new DataView(new ArrayBuffer(1)), + ); + + expect(evaluate("={{encodeURI('https://google.com')}}")).toEqual( + encodeURI('https://google.com'), + ); + expect(evaluate("={{encodeURIComponent('https://google.com')}}")).toEqual( + encodeURIComponent('https://google.com'), + ); + expect(evaluate("={{decodeURI('https://google.com')}}")).toEqual( + decodeURI('https://google.com'), + ); + expect(evaluate("={{decodeURIComponent('https://google.com')}}")).toEqual( + decodeURIComponent('https://google.com'), + ); + + expect(evaluate('={{Boolean(1)}}')).toEqual(Boolean(1)); + expect(evaluate('={{Symbol(1).toString()}}')).toEqual(Symbol(1).toString()); }); - describe('Test all expression transform fixtures', () => { - for (const t of baseFixtures) { - if (!t.tests.some((test) => test.type === 'transform')) { - continue; - } - test(t.expression, () => { - for (const test of t.tests.filter( - (test): test is ExpressionTestTransform => test.type === 'transform', - )) { - const expr = t.expression; - expect(extendSyntax(expr, test.forceTransform)).toEqual(test.result ?? expr); - } - }); - } + it('should not able to do arbitrary code execution', () => { + const testFn = jest.fn(); + Object.assign(global, { testFn }); + expect(() => evaluate("={{ Date['constructor']('testFn()')()}}")).toThrowError( + new ExpressionError('Cannot access "constructor" due to security concerns'), + ); + expect(testFn).not.toHaveBeenCalled(); }); }); -} + + describe('Test all expression value fixtures', () => { + const expression = workflow.expression; + + const evaluate = (value: string, data: INodeExecutionData[]) => { + const itemIndex = data.length === 0 ? -1 : 0; + return expression.getParameterValue(value, null, 0, itemIndex, 'node', data, 'manual', {}); + }; + + for (const t of baseFixtures) { + if (!t.tests.some((test) => test.type === 'evaluation')) { + continue; + } + test(t.expression, () => { + const evaluationTests = t.tests.filter( + (test): test is ExpressionTestEvaluation => test.type === 'evaluation', + ); + + for (const test of evaluationTests) { + const input = test.input.map((d) => ({ json: d })) as any; + + if ('error' in test) { + expect(() => evaluate(t.expression, input)).toThrowError(test.error); + } else { + expect(evaluate(t.expression, input)).toStrictEqual(test.output); + } + } + }); + } + }); + + describe('Test all expression transform fixtures', () => { + for (const t of baseFixtures) { + if (!t.tests.some((test) => test.type === 'transform')) { + continue; + } + test(t.expression, () => { + for (const test of t.tests.filter( + (test): test is ExpressionTestTransform => test.type === 'transform', + )) { + const expr = t.expression; + expect(extendSyntax(expr, test.forceTransform)).toEqual(test.result ?? expr); + } + }); + } + }); +}); diff --git a/packages/workflow/test/ExpressionExtensions/ExpressionExtension.test.ts b/packages/workflow/test/ExpressionExtensions/ExpressionExtension.test.ts index fc107484cb..79c0d84da8 100644 --- a/packages/workflow/test/ExpressionExtensions/ExpressionExtension.test.ts +++ b/packages/workflow/test/ExpressionExtensions/ExpressionExtension.test.ts @@ -30,7 +30,7 @@ describe('Expression Extension Transforms', () => { }); }); -describe('tmpl Expression Parser', () => { +describe('Expression Parser', () => { describe('Compatible splitting', () => { test('Lone expression', () => { expect(splitExpression('{{ "" }}')).toEqual([ diff --git a/packages/workflow/test/ExpressionExtensions/ObjectExtensions.test.ts b/packages/workflow/test/ExpressionExtensions/ObjectExtensions.test.ts index 120e5aa9d7..ef785c9db8 100644 --- a/packages/workflow/test/ExpressionExtensions/ObjectExtensions.test.ts +++ b/packages/workflow/test/ExpressionExtensions/ObjectExtensions.test.ts @@ -1,3 +1,4 @@ +import { ApplicationError } from '@/errors'; import { objectExtensions } from '@/Extensions/ObjectExtensions'; import { evaluate } from './Helpers'; @@ -126,7 +127,9 @@ describe('Data Transformation Functions', () => { test('should not allow prototype pollution', () => { ['{__proto__: {polluted: true}}', '{constructor: {prototype: {polluted: true}}}'].forEach( (testExpression) => { - expect(evaluate(`={{ (${testExpression}).compact() }}`)).toEqual(null); + expect(() => evaluate(`={{ (${testExpression}).compact() }}`)).toThrow( + ApplicationError, + ); expect(({} as any).polluted).toBeUndefined(); }, ); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 77622d902e..f342ab69d5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -717,7 +717,7 @@ importers: version: 4.3.0 '@getzep/zep-cloud': specifier: 1.0.12 - version: 1.0.12(@langchain/core@0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1)))(encoding@0.1.13)(langchain@0.3.11(b4eb53fe8b825d6e8edd96cc3d942586)) + version: 1.0.12(@langchain/core@0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1)))(encoding@0.1.13)(langchain@0.3.11(7f2a4b9c5436679ca8b0df05212b4905)) '@getzep/zep-js': specifier: 0.9.0 version: 0.9.0 @@ -744,7 +744,7 @@ importers: version: 0.3.2(@langchain/core@0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1)))(encoding@0.1.13) '@langchain/community': specifier: 'catalog:' - version: 0.3.24(67fb36bad0bcdd2b0df3579415b33a93) + version: 0.3.24(0b620065402de60ffbc4ade3af2d8197) '@langchain/core': specifier: 'catalog:' version: 0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1)) @@ -846,7 +846,7 @@ importers: version: 23.0.1 langchain: specifier: 0.3.11 - version: 0.3.11(b4eb53fe8b825d6e8edd96cc3d942586) + version: 0.3.11(7f2a4b9c5436679ca8b0df05212b4905) lodash: specifier: 'catalog:' version: 4.17.21 @@ -2489,9 +2489,6 @@ importers: '@n8n/tournament': specifier: 1.0.6 version: 1.0.6 - '@n8n_io/riot-tmpl': - specifier: 4.0.0 - version: 4.0.0 ast-types: specifier: 0.15.2 version: 0.15.2 @@ -4956,9 +4953,6 @@ packages: resolution: {integrity: sha512-HrxdwlZw3MfYrfgvjxIGTyi3gJUCOFHtMcL5OpqpHhwfMGwuBauRgUOt63Twx4qs7f2bnz5yHV3jvuU3mF45UQ==} engines: {node: '>=18.12.1'} - '@n8n_io/riot-tmpl@4.0.0': - resolution: {integrity: sha512-/xw8HQgYQlBCrt3IKpNSSB1CgpP7XArw1QTRjP+KEw+OHT8XGvHxXrW9VGdUu9RwDnzm/LFu+dNLeDmwJMeOwQ==} - '@n8n_io/riot-tmpl@4.0.1': resolution: {integrity: sha512-/zdRbEfTFjsm1NqnpPQHgZTkTdbp5v3VUxGeMA9098sps8jRCTraQkc3AQstJgHUm7ylBXJcIVhnVeLUMWAfwQ==} @@ -16465,7 +16459,7 @@ snapshots: '@gar/promisify@1.1.3': optional: true - '@getzep/zep-cloud@1.0.12(@langchain/core@0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1)))(encoding@0.1.13)(langchain@0.3.11(b4eb53fe8b825d6e8edd96cc3d942586))': + '@getzep/zep-cloud@1.0.12(@langchain/core@0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1)))(encoding@0.1.13)(langchain@0.3.11(7f2a4b9c5436679ca8b0df05212b4905))': dependencies: form-data: 4.0.0 node-fetch: 2.7.0(encoding@0.1.13) @@ -16474,7 +16468,7 @@ snapshots: zod: 3.24.1 optionalDependencies: '@langchain/core': 0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1)) - langchain: 0.3.11(b4eb53fe8b825d6e8edd96cc3d942586) + langchain: 0.3.11(7f2a4b9c5436679ca8b0df05212b4905) transitivePeerDependencies: - encoding @@ -16986,7 +16980,7 @@ snapshots: - aws-crt - encoding - '@langchain/community@0.3.24(67fb36bad0bcdd2b0df3579415b33a93)': + '@langchain/community@0.3.24(0b620065402de60ffbc4ade3af2d8197)': dependencies: '@browserbasehq/stagehand': 1.9.0(@playwright/test@1.49.1)(deepmerge@4.3.1)(dotenv@16.4.5)(encoding@0.1.13)(openai@4.78.1(encoding@0.1.13)(zod@3.24.1))(zod@3.24.1) '@ibm-cloud/watsonx-ai': 1.1.2 @@ -16997,7 +16991,7 @@ snapshots: flat: 5.0.2 ibm-cloud-sdk-core: 5.3.2 js-yaml: 4.1.0 - langchain: 0.3.11(b4eb53fe8b825d6e8edd96cc3d942586) + langchain: 0.3.11(7f2a4b9c5436679ca8b0df05212b4905) langsmith: 0.2.15(openai@4.78.1(encoding@0.1.13)(zod@3.24.1)) openai: 4.78.1(encoding@0.1.13)(zod@3.24.1) uuid: 10.0.0 @@ -17012,7 +17006,7 @@ snapshots: '@aws-sdk/credential-provider-node': 3.808.0 '@azure/storage-blob': 12.26.0 '@browserbasehq/sdk': 2.0.0(encoding@0.1.13) - '@getzep/zep-cloud': 1.0.12(@langchain/core@0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1)))(encoding@0.1.13)(langchain@0.3.11(b4eb53fe8b825d6e8edd96cc3d942586)) + '@getzep/zep-cloud': 1.0.12(@langchain/core@0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1)))(encoding@0.1.13)(langchain@0.3.11(7f2a4b9c5436679ca8b0df05212b4905)) '@getzep/zep-js': 0.9.0 '@google-ai/generativelanguage': 2.6.0(encoding@0.1.13) '@google-cloud/storage': 7.12.1(encoding@0.1.13) @@ -17462,10 +17456,6 @@ snapshots: node-rsa: 1.1.1 undici: 7.7.0 - '@n8n_io/riot-tmpl@4.0.0': - dependencies: - eslint-config-riot: 1.0.0 - '@n8n_io/riot-tmpl@4.0.1': dependencies: eslint-config-riot: 1.0.0 @@ -23203,7 +23193,7 @@ snapshots: '@types/debug': 4.1.12 '@types/node': 18.16.16 '@types/tough-cookie': 4.0.2 - axios: 1.8.3(debug@4.4.0) + axios: 1.8.3 camelcase: 6.3.0 debug: 4.4.0(supports-color@8.1.1) dotenv: 16.4.5 @@ -23213,7 +23203,7 @@ snapshots: isstream: 0.1.2 jsonwebtoken: 9.0.2 mime-types: 2.1.35 - retry-axios: 2.6.0(axios@1.8.3) + retry-axios: 2.6.0(axios@1.8.3(debug@4.4.0)) tough-cookie: 4.1.3 transitivePeerDependencies: - supports-color @@ -24207,7 +24197,7 @@ snapshots: kuler@2.0.0: {} - langchain@0.3.11(b4eb53fe8b825d6e8edd96cc3d942586): + langchain@0.3.11(7f2a4b9c5436679ca8b0df05212b4905): dependencies: '@langchain/core': 0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1)) '@langchain/openai': 0.3.17(@langchain/core@0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1)))(encoding@0.1.13) @@ -26584,7 +26574,7 @@ snapshots: onetime: 5.1.2 signal-exit: 3.0.7 - retry-axios@2.6.0(axios@1.8.3): + retry-axios@2.6.0(axios@1.8.3(debug@4.4.0)): dependencies: axios: 1.8.3