fixup! TF-4224 Reduce ReDoS vulnerability
This commit is contained in:
@@ -24,6 +24,17 @@ class HtmlUtils {
|
||||
static final random = Random();
|
||||
static final htmlUnescape = HtmlUnescape();
|
||||
|
||||
// ReDoS-safe regex patterns as const
|
||||
static final _htmlStartTagRegex = RegExp(r'<[a-zA-Z][^>\s]*[^>]*>');
|
||||
static final _htmlEndTagRegex = RegExp(r'</[a-zA-Z][^>]{0,128}>');
|
||||
static final _whitespaceNormalizationRegex = RegExp(r'\s+');
|
||||
static final _urlRegex = RegExp(
|
||||
r'''(?:https?://|ftp://|mailto:|file://|www\.)[^\s<.]+(?:\.[^\s<.]+)*(?<![.,:;!?"')\]])''',
|
||||
caseSensitive: false,
|
||||
multiLine: true,
|
||||
);
|
||||
static final _protocolRegex = RegExp(r'^(?:https?|ftp|mailto|file)');
|
||||
|
||||
static const removeLineHeight1px = (
|
||||
script: '''
|
||||
document.querySelectorAll('[style*="line-height"]').forEach(el => {
|
||||
@@ -700,8 +711,8 @@ class HtmlUtils {
|
||||
}
|
||||
|
||||
static String addQuoteToggle(String htmlString) {
|
||||
final likelyHtml = htmlString.contains(RegExp(r'<[a-zA-Z][^>\s]*[^>]*>')) && // Contains a start tag
|
||||
htmlString.contains(RegExp(r'</[a-zA-Z][^>]{0,128}>')); // Contains an end tag
|
||||
final likelyHtml = htmlString.contains(_htmlStartTagRegex) && // Contains a start tag
|
||||
htmlString.contains(_htmlEndTagRegex); // Contains an end tag
|
||||
|
||||
if (!likelyHtml) {
|
||||
return htmlString; // Not likely HTML, return original
|
||||
@@ -858,7 +869,7 @@ class HtmlUtils {
|
||||
cleaned = cleaned.replaceAll(tagRegex, ' ');
|
||||
|
||||
// Normalize whitespace
|
||||
cleaned = cleaned.replaceAll(RegExp(r'\s+'), ' ').trim();
|
||||
cleaned = cleaned.replaceAll(_whitespaceNormalizationRegex, ' ').trim();
|
||||
|
||||
return cleaned;
|
||||
}
|
||||
@@ -870,13 +881,7 @@ class HtmlUtils {
|
||||
|
||||
if (container == null) return htmlString;
|
||||
|
||||
final urlRegex = RegExp(
|
||||
r'''(?:https?://|ftp://|mailto:|file://|www\.)[^\s<.]+(?:\.[^\s<.]+)*(?<![.,:;!?"')\]])''',
|
||||
caseSensitive: false,
|
||||
multiLine: true,
|
||||
);
|
||||
|
||||
_processNode(container, urlRegex);
|
||||
_processNode(container, _urlRegex);
|
||||
|
||||
return container.innerHtml;
|
||||
} catch (e) {
|
||||
@@ -899,9 +904,6 @@ class HtmlUtils {
|
||||
};
|
||||
|
||||
static void _processNode(dom.Node node, RegExp urlRegex) {
|
||||
// 1. Anchored with ^ to fail instantly if the start doesn't match
|
||||
// 2. Grouped for clarity
|
||||
final protocolRegex = RegExp(r'^(?:https?|ftp|mailto|file)');
|
||||
for (var child in node.nodes.toList()) {
|
||||
// Skip if node or parent node is in tag to skip
|
||||
final parentTag = child.parent?.localName;
|
||||
@@ -932,7 +934,7 @@ class HtmlUtils {
|
||||
nodes.add(dom.Text(url));
|
||||
} else {
|
||||
// Normalize href
|
||||
final href = url.startsWith(protocolRegex)
|
||||
final href = url.startsWith(_protocolRegex)
|
||||
? url
|
||||
: 'https://$url';
|
||||
|
||||
|
||||
@@ -12,6 +12,15 @@ class StringConvert {
|
||||
// Change the pattern to include newlines directly to avoid the .replaceAll() step
|
||||
static const String emailSeparatorPattern = r'[ ,;\n\r\t]+';
|
||||
|
||||
// ReDoS-safe regex patterns as static final
|
||||
static final _base64ValidationRegex = RegExp(r'^[A-Za-z0-9+/=]+$');
|
||||
static final _mdSeparatorRegex = RegExp(
|
||||
r'^\|?(?:[ \t]*:?-+:?[ \t]*\|)+[ \t]*:?-+:?[ \t]*\|?$',
|
||||
multiLine: false,
|
||||
);
|
||||
static final _asciiArtRegex = RegExp(r'[+\-|/\\=]');
|
||||
static final _namedAddressRegex = RegExp(r'''(?:(?:"([^"]+)"|'([^']+)')\s*)?<([^>]+)>''');
|
||||
|
||||
static String? writeEmptyToNull(String text) {
|
||||
if (text.isEmpty) return null;
|
||||
return text;
|
||||
@@ -40,7 +49,7 @@ class StringConvert {
|
||||
// 2. Base64 Check - Using a non-regex check first is faster
|
||||
if (input.length % 4 == 0 && !input.contains(' ')) {
|
||||
// Only run regex if basic length/whitespace checks pass
|
||||
if (RegExp(r'^[A-Za-z0-9+/=]+$').hasMatch(input)) {
|
||||
if (_base64ValidationRegex.hasMatch(input)) {
|
||||
try {
|
||||
input = utf8.decode(base64.decode(input));
|
||||
} catch (_) {
|
||||
@@ -132,25 +141,14 @@ class StringConvert {
|
||||
text.split('\n').where((line) => line.trim().isNotEmpty).toList();
|
||||
if (lines.length < 2) return false;
|
||||
|
||||
// 1. Hardened Markdown Separator Regex
|
||||
// We use [ \t]* instead of \s to be specific and avoid newline issues
|
||||
// We ensure the dash sequence is clearly delimited
|
||||
final mdSeparatorRegex = RegExp(
|
||||
r'^\|?(?:[ \t]*:?-+:?[ \t]*\|)+[ \t]*:?-+:?[ \t]*\|?$',
|
||||
multiLine: false);
|
||||
|
||||
// 2. Optimized ASCII Check
|
||||
// We use hasMatch instead of split().length for better performance
|
||||
final asciiArtRegex = RegExp(r'[+\-|/\\=]');
|
||||
|
||||
bool isMarkdown = false;
|
||||
bool allLinesHaveAscii = true;
|
||||
|
||||
for (final line in lines) {
|
||||
if (!isMarkdown && mdSeparatorRegex.hasMatch(line)) {
|
||||
if (!isMarkdown && _mdSeparatorRegex.hasMatch(line)) {
|
||||
isMarkdown = true;
|
||||
}
|
||||
if (allLinesHaveAscii && !asciiArtRegex.hasMatch(line)) {
|
||||
if (allLinesHaveAscii && !_asciiArtRegex.hasMatch(line)) {
|
||||
allLinesHaveAscii = false;
|
||||
}
|
||||
// Early exit if we found a table but also know it's not ASCII art
|
||||
@@ -166,8 +164,7 @@ class StringConvert {
|
||||
input = Uri.decodeComponent(input);
|
||||
}
|
||||
|
||||
if (input.length % 4 == 0 &&
|
||||
input.contains(RegExp(r'^[A-Za-z0-9+/=]+$'))) {
|
||||
if (input.length % 4 == 0 && _base64ValidationRegex.hasMatch(input)) {
|
||||
try {
|
||||
input = utf8.decode(base64.decode(input));
|
||||
} catch (_) {}
|
||||
@@ -176,11 +173,9 @@ class StringConvert {
|
||||
input = input.replaceAll('\n', ' ');
|
||||
final results = <NamedAddress>[];
|
||||
|
||||
final pattern = RegExp(r'''(?:(?:"([^"]+)"|'([^']+)')\s*)?<([^>]+)>''');
|
||||
|
||||
int currentIndex = 0;
|
||||
// Use a for-in loop directly on the iterable to save memory
|
||||
for (final match in pattern.allMatches(input)) {
|
||||
for (final match in _namedAddressRegex.allMatches(input)) {
|
||||
if (match.start > currentIndex) {
|
||||
final between = input.substring(currentIndex, match.start);
|
||||
results.addAll(_splitPlainAddresses(between, emailSeparatorPattern));
|
||||
|
||||
Reference in New Issue
Block a user