fixup! TF-4224 Reduce ReDoS vulnerability

This commit is contained in:
Dang Dat
2026-01-05 09:38:14 +07:00
committed by Dat H. Pham
parent dd584c9ce7
commit 8e0276114e
@@ -174,6 +174,134 @@ void main() {
expect(stopwatch.elapsedMilliseconds, lessThan(500));
expect(result, contains('content'));
});
test('Should handle Gmail newsletter HTML structure', () {
// Real pattern from Gmail promotional emails
const gmailHtml = '''
<div dir="ltr">
<table cellpadding="0" cellspacing="0" border="0" width="100%">
<tr>
<td style="background-image: url('https://ci3.googleusercontent.com/proxy/ABCDEFGHIJKLMNOP1234567890abcdefghijklmnop=s0-d-e1-ft#https://example.com/images/header.jpg'); background-size: cover;">
<p style="font-family: Arial, sans-serif;">Hello User,</p>
<p>Visit https://example.com/products for our latest offers.</p>
</td>
</tr>
</table>
</div>
''';
final stopwatch = Stopwatch()..start();
// Extract plain text
final plainText = HtmlUtils.extractPlainText(gmailHtml);
// Wrap URLs
final linkedHtml = HtmlUtils.wrapPlainTextLinks(gmailHtml);
stopwatch.stop();
// Performance check
expect(stopwatch.elapsedMilliseconds, lessThan(200),
reason: 'Gmail HTML processing should be fast');
// Functional check
expect(plainText, contains('Hello User'));
expect(plainText, contains('Visit'));
expect(linkedHtml, contains('<a href='));
});
test('Should handle URLs with complex query parameters', () {
const complexUrls = [
// Tracking parameters
'https://example.com/article?utm_source=email&utm_medium=newsletter&utm_campaign=spring2024',
// Nested URL in parameter
'https://redirect.example.com?url=https://target.com/page&ref=email',
// Fragment identifier
'https://docs.example.com/guide#installation-steps',
// Multiple fragments (invalid but should handle)
'https://example.com/page#section1#subsection',
// Query + Fragment
'https://api.example.com/v1/users?id=123&format=json#results',
// Encoded characters
'https://example.com/search?q=hello%20world&lang=en',
];
for (final url in complexUrls) {
final html = '<p>Visit $url for details</p>';
final stopwatch = Stopwatch()..start();
final result = HtmlUtils.wrapPlainTextLinks(html);
stopwatch.stop();
// Performance check
expect(stopwatch.elapsedMilliseconds, lessThan(100),
reason: 'Should process quickly: $url');
// Functional check
expect(result, contains('<a href='),
reason: 'Should wrap URL: $url');
expect(result, contains(url.split('?')[0]),
reason: 'Should preserve URL base: $url');
}
});
test('Should handle HTML tags at 128 character boundary', () {
final exactly128 = '</div${'x' * 122}>';
// 129 characters (over limit, should still process fast)
final over128 = '</div${'x' * 123}>';
// Very long (1000 chars - malicious)
final malicious = '</div${'x' * 995}>';
final stopwatch1 = Stopwatch()..start();
HtmlUtils.addQuoteToggle('Text $exactly128 end');
stopwatch1.stop();
final stopwatch2 = Stopwatch()..start();
HtmlUtils.addQuoteToggle('Text $over128 end');
stopwatch2.stop();
final stopwatch3 = Stopwatch()..start();
HtmlUtils.addQuoteToggle('Text $malicious end');
stopwatch3.stop();
// All should complete quickly (bounded quantifier prevents backtracking)
expect(stopwatch1.elapsedMilliseconds, lessThan(100),
reason: '128 chars (boundary) should be fast');
expect(stopwatch2.elapsedMilliseconds, lessThan(100),
reason: '129 chars (over boundary) should be fast');
expect(stopwatch3.elapsedMilliseconds, lessThan(100),
reason: '1000 chars (malicious) should be fast');
});
test('Should handle deeply nested HTML structure', () {
// 50 levels (realistic maximum)
final deepNesting = '${'<div>' * 50}<p>Deep content with https://example.com link</p>${'</div>' * 50}';
// 100 levels (extreme)
final extremeNesting = '${'<span>' * 100}Content${'</span>' * 100}';
final stopwatch1 = Stopwatch()..start();
final plainText1 = HtmlUtils.extractPlainText(deepNesting);
final linked1 = HtmlUtils.wrapPlainTextLinks(deepNesting);
stopwatch1.stop();
final stopwatch2 = Stopwatch()..start();
final plainText2 = HtmlUtils.extractPlainText(extremeNesting);
stopwatch2.stop();
// Performance checks
expect(stopwatch1.elapsedMilliseconds, lessThan(200),
reason: '50 levels of nesting should process quickly');
expect(stopwatch2.elapsedMilliseconds, lessThan(300),
reason: '100 levels of nesting should process quickly');
// Functional checks
expect(plainText1, contains('Deep content'));
expect(linked1, contains('<a href='));
expect(plainText2, contains('Content'));
});
});
group('StringConvert regex patterns', () {
@@ -238,6 +366,51 @@ void main() {
expect(result, contains('another@test.com'));
});
test('Should handle emails with special characters and edge cases', () {
final specialCharEmails = [
// Valid special characters in local part
'user+tag@example.com',
'user.name@example.com',
'user_name@example.com',
'user-name@example.com',
'user123@example.com',
'123user@example.com',
// International domain
'user@example.co.uk',
'user@sub.domain.example.com',
// Quoted local part (valid but rare)
'"user@name"@example.com',
'"user name"@example.com',
// Edge cases with dots
'a.b.c.d@example.com',
'user.@example.com', // Invalid but should handle gracefully
'.user@example.com', // Invalid but should handle gracefully
// Unicode/international emails (if supported)
'user@例え.jp',
// Very long local part
'${'a' * 64}@example.com',
// Mixed with other text
'Contact us at support@example.com for help',
'Multiple emails: alice@test.com, bob@test.org',
];
for (final input in specialCharEmails) {
final stopwatch = Stopwatch()..start();
final result = StringConvert.extractEmailAddress(input);
stopwatch.stop();
// Performance check
expect(stopwatch.elapsedMilliseconds, lessThan(100),
reason: 'Should process quickly: $input');
// If it looks like a valid email, should extract something
if (input.contains('@') && !input.startsWith('.') && !input.endsWith('.')) {
expect(result, isNotEmpty,
reason: 'Should extract email from: $input');
}
}
});
test('isTextTable should correctly identify valid tables', () {
const validTables = [
'''