From 120d2e265f16dc8272f3b48111185de3fac1cf01 Mon Sep 17 00:00:00 2001 From: arshiya tabasum Date: Thu, 23 Jul 2026 15:11:04 +0530 Subject: [PATCH] fix(compiler): prevent ReDoS in i18n placeholder name extraction The regex that reads the name from a `//i18n(ph="NAME")` annotation on an interpolation expression chained five greedy `[\s\S]*` segments between the literals `i18n`, `(`, `ph`, `=` and the opening quote. When the prefix matches but the closing quote/paren is missing, a run of `i18n(ph=` text can be split across those wildcards in many ways, so the match backtracks catastrophically (`'//' + 'i18n(ph='.repeat(200)`, ~1.6 KB, takes ~96s). The separators in that annotation are only ever whitespace, so bind them to `\s*` and stop the name at the delimiter quote. Valid annotations are still recognized identically; the match is now linear. --- packages/compiler/src/i18n/i18n_parser.ts | 7 +++++-- packages/compiler/test/i18n/i18n_parser_spec.ts | 11 +++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/packages/compiler/src/i18n/i18n_parser.ts b/packages/compiler/src/i18n/i18n_parser.ts index e1fa97e06513..6f33c653b03b 100644 --- a/packages/compiler/src/i18n/i18n_parser.ts +++ b/packages/compiler/src/i18n/i18n_parser.ts @@ -453,8 +453,11 @@ ${nodes.map((node) => `"${node.sourceSpan.toString()}"`).join('\n')} } } -const _CUSTOM_PH_EXP = - /\/\/[\s\S]*i18n[\s\S]*\([\s\S]*ph[\s\S]*=[\s\S]*("|')([\s\S]*?)\1[\s\S]*\)/g; +// The separators inside a `//i18n(ph="name")` annotation are only ever whitespace, so bind them +// to `\s*` and stop the name at the delimiter quote. The previous `[\s\S]*` between each token let +// a run of `i18n(ph=` characters be partitioned in many ways, so an unterminated annotation made +// this match backtrack catastrophically. +const _CUSTOM_PH_EXP = /\/\/\s*i18n\s*\(\s*ph\s*=\s*("|')((?:(?!\1)[\s\S])*)\1\s*\)/g; function extractPlaceholderName(input: string): string { return input.split(_CUSTOM_PH_EXP)[2]; diff --git a/packages/compiler/test/i18n/i18n_parser_spec.ts b/packages/compiler/test/i18n/i18n_parser_spec.ts index 9cb3968b9fe2..32f6dde0ce43 100644 --- a/packages/compiler/test/i18n/i18n_parser_spec.ts +++ b/packages/compiler/test/i18n/i18n_parser_spec.ts @@ -147,6 +147,17 @@ describe('I18nParser', () => { [[`[before, exp //i18n(ph='teSt') , after]`], 'm', 'd', ''], ]); }); + + it('should not backtrack on an unterminated placeholder annotation', () => { + // A `//i18n(ph=...` annotation that never closes must fall back to the default + // INTERPOLATION placeholder quickly instead of hanging while extracting the name. + const expression = '//' + 'i18n(ph='.repeat(200); + const start = Date.now(); + expect(_humanizeMessages(`
before{{ ${expression} }}after
`)).toEqual([ + [[`[before, ${expression} , after]`], 'm', 'd', ''], + ]); + expect(Date.now() - start).toBeLessThan(2000); + }); }); describe('blocks', () => {