fixup! TF-4224 Reduce ReDoS vulnerability
This commit is contained in:
@@ -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 = [
|
||||
'''
|
||||
|
||||
Reference in New Issue
Block a user