fixup! TF-4224 Reduce ReDoS vulnerability
This commit is contained in:
@@ -174,6 +174,134 @@ void main() {
|
|||||||
expect(stopwatch.elapsedMilliseconds, lessThan(500));
|
expect(stopwatch.elapsedMilliseconds, lessThan(500));
|
||||||
expect(result, contains('content'));
|
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', () {
|
group('StringConvert regex patterns', () {
|
||||||
@@ -238,6 +366,51 @@ void main() {
|
|||||||
expect(result, contains('another@test.com'));
|
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', () {
|
test('isTextTable should correctly identify valid tables', () {
|
||||||
const validTables = [
|
const validTables = [
|
||||||
'''
|
'''
|
||||||
|
|||||||
Reference in New Issue
Block a user