diff --git a/packages/@n8n/tournament/src/ExpressionSplitter.ts b/packages/@n8n/tournament/src/ExpressionSplitter.ts index 10970b0564f..0c27b0f09e9 100644 --- a/packages/@n8n/tournament/src/ExpressionSplitter.ts +++ b/packages/@n8n/tournament/src/ExpressionSplitter.ts @@ -17,7 +17,7 @@ const OPEN_BRACKET = /(?\{\{)/; const CLOSE_BRACKET = /(?\}\})/; export const escapeCode = (text: string): string => { - return text.replace('\\}}', '}}'); + return text.replace(/\\\}\}/g, '}}'); }; const normalizeBackslashes = (text: string): string => { @@ -99,7 +99,7 @@ export const splitExpression = (expression: string): ExpressionChunk[] => { // Expressions only have closing brackets escaped const escapeTmplExpression = (part: string) => { - return part.replace('}}', '\\}}'); + return part.replace(/\}\}/g, '\\}}'); }; export const joinExpression = (parts: ExpressionChunk[]): string => { diff --git a/packages/@n8n/tournament/test/Expression.test.ts b/packages/@n8n/tournament/test/Expression.test.ts index b336b133334..3430933695c 100644 --- a/packages/@n8n/tournament/test/Expression.test.ts +++ b/packages/@n8n/tournament/test/Expression.test.ts @@ -14,4 +14,8 @@ describe('Expression', () => { evaluator.execute('{{ import("").then(fs => fs.writeFileSync("/tmp/flag", "flag")) }}', {}), ).toThrow('Imports are not supported'); }); + + test('Should unescape every escaped closing bracket', () => { + expect(evaluator.execute('{{ [{ a: { b: 1 \\}}, { a: { b: 2 \\}}][1].a.b }}', {})).toBe(2); + }); }); diff --git a/packages/@n8n/tournament/test/ExpressionSplitter.test.ts b/packages/@n8n/tournament/test/ExpressionSplitter.test.ts index 82db09dd4ae..93bc646e926 100644 --- a/packages/@n8n/tournament/test/ExpressionSplitter.test.ts +++ b/packages/@n8n/tournament/test/ExpressionSplitter.test.ts @@ -43,6 +43,13 @@ describe('tmpl Expression Parser', () => { ]); }); + test('Multiple escaped closing brackets', () => { + expect(splitExpression('{{ [{ a: { b: 1 \\}}, { a: { b: 2 \\}}] }}')).toEqual([ + { type: 'text', text: '' }, + { type: 'code', text: ' [{ a: { b: 1 }}, { a: { b: 2 }}] ', hasClosingBrackets: true }, + ]); + }); + test('Escaped backslashes before double opening curly braces', () => { const expr = 'C:\\\\Users\\\\Administrator\\\\Desktop\\\\abc\\\\{{ $json.files[0].fileName }}'; @@ -92,5 +99,14 @@ describe('tmpl Expression Parser', () => { 'test {{ code.test("\\}}") }}', ); }); + + test('Multiple escaped closing brackets', () => { + expect( + joinExpression([ + { type: 'text', text: '' }, + { type: 'code', text: ' [{ a: { b: 1 }}, { a: { b: 2 }}] ', hasClosingBrackets: true }, + ]), + ).toEqual('{{ [{ a: { b: 1 \\}}, { a: { b: 2 \\}}] }}'); + }); }); }); diff --git a/packages/workflow/src/extensions/expression-parser.ts b/packages/workflow/src/extensions/expression-parser.ts index 66a7b12acc5..550122ffb9d 100644 --- a/packages/workflow/src/extensions/expression-parser.ts +++ b/packages/workflow/src/extensions/expression-parser.ts @@ -18,7 +18,7 @@ const OPEN_BRACKET = /(?\\|)(?\{\{)/; const CLOSE_BRACKET = /(?\\|)(?\}\})/; export const escapeCode = (text: string): string => { - return text.replace('\\}}', '}}'); + return text.replace(/\\\}\}/g, '}}'); }; export const splitExpression = (expression: string): ExpressionChunk[] => { @@ -91,7 +91,7 @@ export const splitExpression = (expression: string): ExpressionChunk[] => { // Expressions only have closing brackets escaped const escapeTmplExpression = (part: string) => { - return part.replace('}}', '\\}}'); + return part.replace(/\}\}/g, '\\}}'); }; export const joinExpression = (parts: ExpressionChunk[]): string => { diff --git a/packages/workflow/test/ExpressionExtensions/expression-extension.test.ts b/packages/workflow/test/ExpressionExtensions/expression-extension.test.ts index 68122186f1b..2361173befd 100644 --- a/packages/workflow/test/ExpressionExtensions/expression-extension.test.ts +++ b/packages/workflow/test/ExpressionExtensions/expression-extension.test.ts @@ -72,6 +72,13 @@ describe('Expression Parser', () => { ]); }); + test('Multiple escaped closing brackets', () => { + expect(splitExpression('{{ [{ a: { b: 1 \\}}, { a: { b: 2 \\}}] }}')).toEqual([ + { type: 'text', text: '' }, + { type: 'code', text: ' [{ a: { b: 1 }}, { a: { b: 2 }}] ', hasClosingBrackets: true }, + ]); + }); + test('Empty input (CAT-3075)', () => { expect(splitExpression('')).toEqual([{ type: 'text', text: '' }]); }); @@ -107,6 +114,15 @@ describe('Expression Parser', () => { 'test {{ code.test("\\}}") }}', ); }); + + test('Multiple escaped closing brackets', () => { + expect( + joinExpression([ + { type: 'text', text: '' }, + { type: 'code', text: ' [{ a: { b: 1 }}, { a: { b: 2 }}] ', hasClosingBrackets: true }, + ]), + ).toEqual('{{ [{ a: { b: 1 \\}}, { a: { b: 2 \\}}] }}'); + }); }); describe('Edge cases', () => {