From a8f90ec700979e62486a0ff14a606844dc42dfd5 Mon Sep 17 00:00:00 2001 From: Dang Dat Date: Wed, 24 Dec 2025 15:05:45 +0700 Subject: [PATCH] fixup! TF-4224 Reduce ReDoS vulnerability --- ...lize_line_height_in_style_transformer.dart | 14 +++++----- core/lib/utils/html/html_utils.dart | 11 ++++---- core/test/utils/string_convert_test.dart | 26 +++++++++++++++++++ 3 files changed, 39 insertions(+), 12 deletions(-) diff --git a/core/lib/presentation/utils/html_transformer/dom/normalize_line_height_in_style_transformer.dart b/core/lib/presentation/utils/html_transformer/dom/normalize_line_height_in_style_transformer.dart index cef2e672f..53eccd7c8 100644 --- a/core/lib/presentation/utils/html_transformer/dom/normalize_line_height_in_style_transformer.dart +++ b/core/lib/presentation/utils/html_transformer/dom/normalize_line_height_in_style_transformer.dart @@ -8,6 +8,12 @@ class NormalizeLineHeightInStyleTransformer extends DomTransformer { static const _removableLineHeights = ['1px', '100%']; static final _multipleSpacesRegex = RegExp(r' {2,}'); + static final _lineHeightPattern = RegExp( + r'line-height\s*:\s*(?:' + + _removableLineHeights.map(RegExp.escape).join('|') + + r')\s*;?', + caseSensitive: false, + ); @override Future process({ @@ -16,11 +22,7 @@ class NormalizeLineHeightInStyleTransformer extends DomTransformer { Map? mapUrlDownloadCID, }) async { try { - final alternation = _removableLineHeights.map(RegExp.escape).join('|'); - final pattern = RegExp( - r'line-height\s*:\s*(?:' + alternation + r')\s*;?', - caseSensitive: false, - ); + final elements = document.querySelectorAll('[style*="line-height"]'); @@ -28,7 +30,7 @@ class NormalizeLineHeightInStyleTransformer extends DomTransformer { final originalStyle = element.attributes['style']; if (originalStyle == null) continue; - var updatedStyle = originalStyle.replaceAll(pattern, '').trim(); + var updatedStyle = originalStyle.replaceAll(_lineHeightPattern, '').trim(); // Remove extra spaces (>=2 spaces → 1 space) updatedStyle = updatedStyle.replaceAll(_multipleSpacesRegex, ' '); diff --git a/core/lib/utils/html/html_utils.dart b/core/lib/utils/html/html_utils.dart index baba7a2a1..794f5fb9c 100644 --- a/core/lib/utils/html/html_utils.dart +++ b/core/lib/utils/html/html_utils.dart @@ -34,6 +34,10 @@ class HtmlUtils { multiLine: true, ); static final _protocolRegex = RegExp(r'^(?:https?|ftp|mailto|file)'); + static final _tagRemovalRegex = RegExp( + ']*)?>', + caseSensitive: false, + ); static const removeLineHeight1px = ( script: ''' @@ -861,12 +865,7 @@ class HtmlUtils { } while (decoded != cleaned && iterations < maxIterations); // Delete all remaining HTML tags → replace tag with space to avoid text sticking - final String tagsPipe = validTags.map(RegExp.escape).join('|'); - final tagRegex = RegExp( - ']*)?>', - caseSensitive: false, - ); - cleaned = cleaned.replaceAll(tagRegex, ' '); + cleaned = cleaned.replaceAll(_tagRemovalRegex, ' '); // Normalize whitespace cleaned = cleaned.replaceAll(_whitespaceNormalizationRegex, ' ').trim(); diff --git a/core/test/utils/string_convert_test.dart b/core/test/utils/string_convert_test.dart index d3dde6e70..fba8ee1e6 100644 --- a/core/test/utils/string_convert_test.dart +++ b/core/test/utils/string_convert_test.dart @@ -815,6 +815,32 @@ void main() { ]), ); }); + test('Does not corrupt plain text that looks like base64', () { + // "useruser" is 8 chars (multiple of 4) and fits the base64 regex. + // base64.decode("useruser") produces random bytes. + const input = 'useruser'; + final result = StringConvert.extractNamedAddresses(input); + expect(result.first.address, equals('useruser')); + }); + test('Handles brackets with no name', () { + const input = ''; + final result = StringConvert.extractNamedAddresses(input); + expect(result.first.name, isEmpty); + expect(result.first.address, equals('only-email@example.com')); + }); + test('Handles names containing commas', () { + const input = + '"Doe, John" , "Smith, Jane" '; + final result = StringConvert.extractNamedAddresses(input); + expect(result[0].name, equals('Doe, John')); + expect(result[1].name, equals('Smith, Jane')); + }); + test('Handles malformed input gracefully', () { + const input = '"Unclosed Quote '; + final result = StringConvert.extractNamedAddresses(input); + // Ensure it doesn't crash and returns at least the email + expect(result, isNotEmpty); + }); }); group('StringConvert.convertHtmlContentToTextContent', () {