fix(mu): email content not displayed due to sanitize html

(cherry picked from commit 2f06e52d81864a62d6e588bb24a6845e20371bf4)
This commit is contained in:
dab246
2025-12-15 14:32:02 +07:00
committed by Dat H. Pham
parent 50671b98ee
commit 6f455cc728
10 changed files with 362 additions and 141 deletions
+2 -2
View File
@@ -1020,10 +1020,10 @@ packages:
description: description:
path: sanitize_html path: sanitize_html
ref: support_mail ref: support_mail
resolved-ref: fda32cde4d4baadaa988477f498ab6622ee79987 resolved-ref: "6627e13f2236bdea5d25f648e6d55b214a4569df"
url: "https://github.com/linagora/dart-neats.git" url: "https://github.com/linagora/dart-neats.git"
source: git source: git
version: "2.1.0" version: "3.0.0"
shelf: shelf:
dependency: transitive dependency: transitive
description: description:
@@ -16,7 +16,7 @@ class SanitizeHyperLinkTagInHtmlTransformer extends DomTransformer {
Map<String, String>? mapUrlDownloadCID, Map<String, String>? mapUrlDownloadCID,
}) async { }) async {
try { try {
final elements = document.querySelectorAll('a'); final elements = document.querySelectorAll('a[href]');
if (elements.isEmpty) return; if (elements.isEmpty) return;
@@ -3,40 +3,13 @@ import 'package:core/presentation/utils/html_transformer/base/text_transformer.d
import 'package:core/presentation/utils/html_transformer/sanitize_html.dart'; import 'package:core/presentation/utils/html_transformer/sanitize_html.dart';
class StandardizeHtmlSanitizingTransformers extends TextTransformer { class StandardizeHtmlSanitizingTransformers extends TextTransformer {
static final SanitizeHtml _sanitizer = SanitizeHtml();
static const List<String> mailAllowedHtmlAttributes = [
'style',
'public-asset-id',
'data-filename',
'bgcolor',
'id',
'class',
'data-mimetype',
];
static const List<String> mailAllowedHtmlTags = [
'font',
'u',
'center',
'style',
'body',
'section',
'google-sheets-html-origin',
'colgroup',
'col',
'nav',
'main',
'footer',
'supress_time_adjustment',
];
const StandardizeHtmlSanitizingTransformers(); const StandardizeHtmlSanitizingTransformers();
@override @override
String process(String text, HtmlEscape htmlEscape) => String process(String text, HtmlEscape htmlEscape) {
SanitizeHtml().process( if (text.isEmpty) return '';
inputHtml: text, return _sanitizer.process(inputHtml: text);
allowAttributes: mailAllowedHtmlAttributes, }
allowTags: mailAllowedHtmlTags,
);
} }
+2 -2
View File
@@ -973,10 +973,10 @@ packages:
description: description:
path: sanitize_html path: sanitize_html
ref: support_mail ref: support_mail
resolved-ref: fda32cde4d4baadaa988477f498ab6622ee79987 resolved-ref: "6627e13f2236bdea5d25f648e6d55b214a4569df"
url: "https://github.com/linagora/dart-neats.git" url: "https://github.com/linagora/dart-neats.git"
source: git source: git
version: "2.1.0" version: "3.0.0"
shelf: shelf:
dependency: transitive dependency: transitive
description: description:
@@ -1,12 +1,13 @@
import 'package:core/presentation/utils/html_transformer/text/standardize_html_sanitizing_transformers.dart';
import 'package:flutter_test/flutter_test.dart';
import 'dart:convert'; import 'dart:convert';
import 'package:flutter_test/flutter_test.dart';
import 'package:core/presentation/utils/html_transformer/text/standardize_html_sanitizing_transformers.dart';
void main() { void main() {
group('StandardizeHtmlSanitizingTransformers.process', () { group('StandardizeHtmlSanitizingTransformers.process', () {
const transformer = StandardizeHtmlSanitizingTransformers(); const transformer = StandardizeHtmlSanitizingTransformers();
const htmlEscape = HtmlEscape(); const htmlEscape = HtmlEscape();
const listHTMLTags = [
const allowedTags = <String>[
'div', 'div',
'span', 'span',
'p', 'p',
@@ -16,12 +17,10 @@ void main() {
'font', 'font',
'u', 'u',
'center', 'center',
'style',
'section', 'section',
'google-sheets-html-origin',
'supress_time_adjustment',
]; ];
const listOnEventAttributes = [
const eventAttributes = <String>[
'mousedown', 'mousedown',
'mouseenter', 'mouseenter',
'mouseleave', 'mouseleave',
@@ -80,124 +79,368 @@ void main() {
'touchcancel', 'touchcancel',
]; ];
test('SHOULD remove all `on*` attributes tag', () { String sanitize(String input) =>
for (var i = 0; i < listOnEventAttributes.length; i++) { transformer.process(input, htmlEscape).trim();
final inputHtml = '<img src="1" href="1" on${listOnEventAttributes[i]}="javascript:alert(1)">';
final result = transformer.process(inputHtml, htmlEscape);
expect(result, equals('<img src="1">')); group('Event attributes, script, iframe fully sanitized', () {
} test('SHOULD remove all on* event attributes from IMG WHEN event attributes exist', () {
}); for (final evt in eventAttributes) {
final html = '<img src="1" href="1" on$evt="javascript:alert(1)">';
test('SHOULD remove all `on*` attributes for any tags', () { expect(sanitize(html), equals('<img src="1">'));
for (var tag in listHTMLTags) {
for (var event in listOnEventAttributes) {
final inputHtml = '<$tag on$event="javascript:alert(1)"></$tag>';
final result = transformer.process(inputHtml, htmlEscape);
expect(result, equals('<$tag></$tag>'));
} }
} });
test('SHOULD remove all on* event attributes FROM any allowed tag FOR all events', () {
for (final tag in allowedTags) {
for (final evt in eventAttributes) {
final html = '<$tag on$evt="javascript:alert(1)"></$tag>';
expect(sanitize(html), equals('<$tag></$tag>'));
}
}
});
test('SHOULD remove all on* event attributes FROM <colgroup>', () {
for (final evt in eventAttributes) {
final html = '<table><colgroup on$evt="javascript:alert(1)"></colgroup></table>';
expect(sanitize(html), equals('<table><colgroup></colgroup></table>'));
}
});
test('SHOULD remove all on* event attributes FROM <col>', () {
for (final evt in eventAttributes) {
final html =
'<table><colgroup><col on$evt="javascript:alert(1)"></colgroup></table>';
expect(sanitize(html),
equals('<table><colgroup><col></colgroup></table>'));
}
});
test('SHOULD remove <script> elements entirely', () {
expect(sanitize('<script>alert("x")</script>'), equals(''));
});
test('SHOULD remove <iframe> elements entirely', () {
expect(sanitize('<iframe onmouseover="prompt(1)"></iframe>'), equals(''));
});
test('SHOULD remove nested <script> elements', () {
const html = '<div><p>Hello<script>alert(1)</script></p></div>';
expect(sanitize(html), equals('<div><p>Hello</p></div>'));
});
}); });
test('SHOULD remove all `on*` attributes for `colgroup` tag', () { group('Href/src sanitization unsafe removed, safe preserved', () {
for (var event in listOnEventAttributes) { test('SHOULD remove javascript: href FROM <a>', () {
final inputHtml = '<table><colgroup on$event="javascript:alert(1)"></colgroup></table>'; const html = '<a href="javascript:alert(1)" id="id1">test</a>';
final result = transformer.process(inputHtml, htmlEscape); expect(sanitize(html), equals('<a id="id1">test</a>'));
});
expect(result, equals('<table><colgroup></colgroup></table>')); test('SHOULD remove invalid JS event attributes FROM <img>', () {
} const html = '<img src="1" href="1" onerror="javascript:alert(1)">';
expect(sanitize(html), equals('<img src="1">'));
});
test('SHOULD keep base64 image sources intact', () {
const html = '<img src="data:image/jpeg;base64,AAAABBBBCCCC==">';
expect(
sanitize(html),
equals('<img src="data:image/jpeg;base64,AAAABBBBCCCC==">'));
});
test('SHOULD keep cid: image sources intact', () {
expect(
sanitize('<img src="cid:email123">'),
equals('<img src="cid:email123">'));
});
}); });
test('SHOULD remove all `on*` attributes for `col` tag', () { group('Unknown tags, structural tags unwrap or preserve safely', () {
for (var event in listOnEventAttributes) { test('SHOULD preserve nav/main/footer but strip dangerous attributes', () {
final inputHtml = '<table><colgroup><col on$event="javascript:alert(1)"></colgroup></table>'; expect(sanitize('<nav href="javascript:1"></nav>'), equals('<nav></nav>'));
final result = transformer.process(inputHtml, htmlEscape); expect(sanitize('<main href="javascript:1"></main>'), equals('<main></main>'));
expect(sanitize('<footer href="javascript:1"></footer>'), equals('<footer></footer>'));
});
expect(result, equals('<table><colgroup><col></colgroup></table>')); test('SHOULD unwrap <supress_time_adjustment> AND keep children', () {
} const html =
'<supress_time_adjustment href="javascript:1"><div><p>Hello</p></div></supress_time_adjustment>';
expect(sanitize(html), equals('<div><p>Hello</p></div>'));
});
test('SHOULD unwrap <google-sheets-html-origin> AND keep children', () {
const html =
'<google-sheets-html-origin href="javascript:1"><div><p>Hello</p></div></google-sheets-html-origin>';
expect(sanitize(html), equals('<div><p>Hello</p></div>'));
});
test('SHOULD unwrap unknown tags AND keep children', () {
expect(sanitize('<custom><p>Hello</p></custom>'), equals('<p>Hello</p>'));
});
test('SHOULD unwrap nested unknown tags AND keep children', () {
expect(
sanitize('<x1><x2><p>Hello</p></x2></x1>'),
equals('<p>Hello</p>'));
});
}); });
test('SHOULD remove attributes of IMG tag WHEN they are invalid', () { group('CSS sanitization unsafe styles removed', () {
const inputHtml = '<img src="1" href="1" onerror="javascript:alert(1)">'; test('SHOULD keep plain text containing JS-like keywords', () {
final result = transformer.process(inputHtml, htmlEscape); const html = 'hello <p>document.cookie</p> world';
expect(result, equals('<img src="1">')); expect(
sanitize(html),
equals('hello <p>document.cookie</p> world'),
);
});
test('SHOULD remove HTML comments entirely', () {
const html = '<div><!--test--><p>A</p><!--x--></div>';
expect(sanitize(html), equals('<div><p>A</p></div>'));
});
test('SHOULD remove unsafe CSS FROM <style>', () {
const html =
'<style>p{position:absolute;color:red;background:url(javascript:evil);}</style>';
expect(sanitize(html), equals(''));
});
test('SHOULD sanitize inline CSS by removing dangerous properties', () {
const html =
'<p style="color:red;position:absolute;background:url(javascript:evil)">X</p>';
expect(sanitize(html), equals('<p style="color: red">X</p>'));
});
}); });
test('SHOULD remove all SCRIPTS tags', () { group('ID and class validation valid preserved, invalid removed', () {
const inputHtml = '<script>alert("This is an alert message!");</script>'; test('SHOULD preserve valid id/class values', () {
final result = transformer.process(inputHtml, htmlEscape).trim(); const html = '<div id="abc123" class="valid_class"></div>';
expect(
sanitize(html),
equals('<div id="abc123" class="valid_class"></div>'));
});
expect(result, equals('')); test('SHOULD remove invalid id/class values', () {
const html = '<div id="!!!" class="@@@"></div>';
expect(sanitize(html), equals('<div></div>'));
});
}); });
test('SHOULD remove all IFRAME tags', () { group('Complex nested HTML sanitized safely', () {
const inputHtml = '<iframe style="xg-p:absolute;top:0;left:0;width:100%;height:100%" onmouseover="prompt(1)">'; test('SHOULD remove all dangerous content AND preserve safe nodes', () {
final result = transformer.process(inputHtml, htmlEscape); const html = '''
<div onclick="x">
<p>Hello<script>alert(1)</script></p>
<img src="cid:x1" onerror="hack()">
<a href="javascript:evil()" id="ok">link</a>
</div>
''';
expect(result, equals('')); final out = sanitize(html);
expect(out.contains('javascript'), false);
expect(out.contains('hack'), false);
expect(out.contains('script'), false);
expect(out.contains('evil'), false);
expect(out.contains('onclick'), false);
expect(out.contains('<img'), true);
expect(out.contains('<div'), true);
expect(out.contains('<a'), true);
expect(out.contains('Hello'), true);
});
}); });
test('SHOULD remove href attribute of A tag WHEN it is invalid', () { group('FORM attributes dangerous removed', () {
const inputHtml = '<a href="javascript:alert(1)" id="id1">test</a>'; final dangerousFormAttributes = [
final result = transformer.process(inputHtml, htmlEscape); 'action="https://malicious.com/x"',
'action="javascript:1"',
'method="POST"',
'target="_blank"',
'enctype="multipart/form-data"',
'formaction="https://evil.com"',
'formmethod="POST"',
'formtarget="_blank"',
'formenctype="text/plain"',
'autocomplete="off"',
'novalidate',
'accept-charset="UTF-8"',
'name="myForm"',
];
expect(result, equals('<a id="id1">test</a>')); test('SHOULD remove every dangerous FORM attribute', () {
for (final attr in dangerousFormAttributes) {
final html = '<form $attr></form>';
expect(sanitize(html), equals(''));
}
});
}); });
test('SHOULD persist value src attribute of IMG tag WHEN it is base64 string', () { group('FORM child elements all removed', () {
const inputHtml = '<img src="data:image/jpeg;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA">'; final forbiddenChildren = [
final result = transformer.process(inputHtml, htmlEscape); '<input type="text">',
'<input type="password">',
'<input type="hidden">',
'<input type="file">',
'<button>Submit</button>',
'<textarea></textarea>',
'<select><option>A</option></select>',
'<option>A</option>',
];
expect(result, equals('<img src="data:image/jpeg;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA">')); test('SHOULD remove dangerous FORM child elements', () {
for (final child in forbiddenChildren) {
expect(sanitize('<form>$child</form>'), equals(''));
}
});
}); });
test('SHOULD persist value src attribute of IMG tag WHEN it is CID string', () { group('FORM combined vectors fully sanitized', () {
const inputHtml = '<img src="cid:email123">'; test('SHOULD remove dangerous FORM attributes AND dangerous children', () {
final result = transformer.process(inputHtml, htmlEscape); const html =
'<form action="https://evil.com" method="POST"><input type="text"></form>';
expect(sanitize(html), equals(''));
});
expect(result, equals('<img src="cid:email123">')); test('SHOULD remove multiple dangerous items inside complex FORM', () {
const html = '''
<form action="javascript:1" method="POST">
<input type="text">
<input type="password">
<button>Submit</button>
</form>
''';
final out = sanitize(html);
expect(out.contains('input'), isFalse);
expect(out.contains('button'), isFalse);
expect(out.contains('action'), isFalse);
expect(out.contains('method'), isFalse);
});
test('SHOULD remove nested FORM elements', () {
const html = '<form action="outer"><form action="inner"></form></form>';
final out = sanitize(html);
expect(out.contains('action'), isFalse);
});
test('SHOULD keep safe nested elements even when FORM wrapper is removed', () {
const html =
'<form action="test"><div><div><p>Content</p></div></div></form>';
final out = sanitize(html);
expect(out.contains('action'), isFalse);
expect(out.contains('<p>'), isTrue);
});
}); });
test( group('FORM safe attributes safely unwrapped', () {
'SHOULD persist nav tag and remove href attribute of A tag ' test('SHOULD unwrap FORM with safe style', () {
'WHEN href is invalid', expect(sanitize('<form style="color: red;"></form>'), contains(''));
() { });
const inputHtml = '<nav href="javascript:alert(1)"></nav>';
final result = transformer.process(inputHtml, htmlEscape);
expect(result, equals('<nav></nav>')); test('SHOULD unwrap FORM with safe class', () {
expect(sanitize('<form class="email-form"></form>'), contains(''));
});
test('SHOULD unwrap FORM with safe id', () {
expect(sanitize('<form id="my-form"></form>'), contains(''));
});
test('SHOULD unwrap FORM with safe bgcolor', () {
expect(sanitize('<form bgcolor="#fff"></form>'), contains(''));
});
test('SHOULD remove dangerous attributes even when safe attributes exist', () {
const html =
'<form style="color:red;" action="https://evil" class="a" method="POST"></form>';
final out = sanitize(html);
expect(out.contains('style'), isFalse);
expect(out.contains('class'), isFalse);
expect(out.contains('action'), isFalse);
expect(out.contains('method'), isFalse);
});
test('SHOULD preserve nested safe elements when FORM wrapper is removed', () {
const html =
'<form><div><p>Safe</p><table><tr><td>X</td></tr></table></div></form>';
final out = sanitize(html);
expect(out.contains('<div>'), isTrue);
expect(out.contains('Safe'), isTrue);
expect(out.contains('<table>'), isTrue);
});
}); });
test( group('Real-world phishing/XSS form attacks fully sanitized', () {
'SHOULD persist main tag and remove href attribute of A tag ' test('SHOULD remove credential phishing forms', () {
'WHEN href is invalid', const html = '''
() { <form action="https://attacker.com" method="POST">
const inputHtml = '<main href="javascript:alert(1)"></main>'; <input type="text">
final result = transformer.process(inputHtml, htmlEscape); <input type="password">
<button>Login</button>
</form>
''';
expect(result, equals('<main></main>')); final out = sanitize(html);
}); expect(out.contains('action'), isFalse);
expect(out.contains('input'), isFalse);
expect(out.contains('button'), isFalse);
});
test( test('SHOULD prevent CSRF token stealing by removing hidden inputs', () {
'SHOULD persist footer tag and remove href attribute of A tag ' const html =
'WHEN href is invalid', '<form action="https://bank.com"><input type="hidden" name="csrf"></form>';
() { final out = sanitize(html);
const inputHtml = '<footer href="javascript:alert(1)"></footer>'; expect(out.contains('input'), isFalse);
final result = transformer.process(inputHtml, htmlEscape); expect(out.contains('csrf'), isFalse);
});
expect(result, equals('<footer></footer>')); test('SHOULD remove javascript-based form actions', () {
}); expect(sanitize('<form action="javascript:alert(1)"></form>'), equals(''));
});
test( test('SHOULD remove data URI form actions', () {
'SHOULD persist supress_time_adjustment tag and remove href attribute of A tag ' expect(sanitize('<form action="data:text/html,<script>x</script>"></form>'),
'WHEN href is invalid', equals(''));
() { });
const inputHtml = '<supress_time_adjustment href="javascript:alert(1)"></supress_time_adjustment>';
final result = transformer.process(inputHtml, htmlEscape);
expect(result, equals('<supress_time_adjustment></supress_time_adjustment>')); test('SHOULD prevent file upload exploitation', () {
const html =
'<form action="/upload" enctype="multipart/form-data"><input type="file"></form>';
final out = sanitize(html);
expect(out.contains('input'), isFalse);
expect(out.contains('enctype'), isFalse);
});
test('SHOULD allow safe cosmetic FORM usage while removing wrapper', () {
const html = '''
<form style="border:1px solid #ccc">
<div><p>This is a survey notification</p></div>
</form>
''';
final out = sanitize(html);
expect(out.contains('<form'), isFalse);
expect(out.contains('style'), isFalse);
expect(out.contains('This is a survey notification'), isTrue);
expect(out.contains('<div'), isTrue);
expect(out.contains('<p'), isTrue);
});
test('SHOULD remove mixed XSS and phishing vectors inside FORM', () {
const html = '''
<form action="https://evil.com" onsubmit="alert(1)">
<script>steal()</script>
<input type="text" onclick="hack()">
<iframe src="x"></iframe>
</form>
''';
final out = sanitize(html);
expect(out.contains('action'), isFalse);
expect(out.contains('onsubmit'), isFalse);
expect(out.contains('script'), isFalse);
expect(out.contains('input'), isFalse);
expect(out.contains('iframe'), isFalse);
});
}); });
}); });
} }
@@ -4,6 +4,7 @@ import 'package:core/data/constants/constant.dart';
import 'package:core/presentation/utils/html_transformer/html_transform.dart'; import 'package:core/presentation/utils/html_transformer/html_transform.dart';
import 'package:core/presentation/utils/html_transformer/text/persist_preformatted_text_transformer.dart'; import 'package:core/presentation/utils/html_transformer/text/persist_preformatted_text_transformer.dart';
import 'package:core/presentation/utils/html_transformer/text/sanitize_autolink_html_transformers.dart'; import 'package:core/presentation/utils/html_transformer/text/sanitize_autolink_html_transformers.dart';
import 'package:core/presentation/utils/html_transformer/text/standardize_html_sanitizing_transformers.dart';
import 'package:core/presentation/utils/html_transformer/transform_configuration.dart'; import 'package:core/presentation/utils/html_transformer/transform_configuration.dart';
import 'package:core/utils/app_logger.dart'; import 'package:core/utils/app_logger.dart';
import 'package:core/utils/string_convert.dart'; import 'package:core/utils/string_convert.dart';
@@ -48,6 +49,7 @@ class HtmlAnalyzer {
final message = _htmlTransform.transformToTextPlain( final message = _htmlTransform.transformToTextPlain(
content: emailContent.content, content: emailContent.content,
transformConfiguration: TransformConfiguration.fromTextTransformers([ transformConfiguration: TransformConfiguration.fromTextTransformers([
const StandardizeHtmlSanitizingTransformers(),
const SanitizeAutolinkHtmlTransformers(), const SanitizeAutolinkHtmlTransformers(),
const PersistPreformattedTextTransformer(), const PersistPreformattedTextTransformer(),
]), ]),
@@ -1175,11 +1175,16 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
consumeState(_parseCalendarEventInteractor!.execute( consumeState(_parseCalendarEventInteractor!.execute(
accountId, accountId,
blobIds, blobIds,
TransformConfiguration.fromTextTransformers(const [ TransformConfiguration.create(
SanitizeAutolinkUnescapeHtmlTransformer(), customTextTransformers: const [
StandardizeHtmlSanitizingTransformers(), SanitizeAutolinkUnescapeHtmlTransformer(),
NewLineTransformer(), StandardizeHtmlSanitizingTransformers(),
]) NewLineTransformer(),
],
customDomTransformers: [
SanitizeHyperLinkTagInHtmlTransformer(),
]
)
)); ));
} }
+2 -2
View File
@@ -997,10 +997,10 @@ packages:
description: description:
path: sanitize_html path: sanitize_html
ref: support_mail ref: support_mail
resolved-ref: fda32cde4d4baadaa988477f498ab6622ee79987 resolved-ref: "6627e13f2236bdea5d25f648e6d55b214a4569df"
url: "https://github.com/linagora/dart-neats.git" url: "https://github.com/linagora/dart-neats.git"
source: git source: git
version: "2.1.0" version: "3.0.0"
shelf: shelf:
dependency: transitive dependency: transitive
description: description:
+2 -2
View File
@@ -1997,10 +1997,10 @@ packages:
description: description:
path: sanitize_html path: sanitize_html
ref: support_mail ref: support_mail
resolved-ref: fda32cde4d4baadaa988477f498ab6622ee79987 resolved-ref: "6627e13f2236bdea5d25f648e6d55b214a4569df"
url: "https://github.com/linagora/dart-neats.git" url: "https://github.com/linagora/dart-neats.git"
source: git source: git
version: "2.1.0" version: "3.0.0"
scribe: scribe:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -316,10 +316,9 @@ void main() {
// arrange // arrange
const eventDescription = '\nhttps://example1.com\nhttps://example2.com'; const eventDescription = '\nhttps://example1.com\nhttps://example2.com';
const expectedEventDescription = '<html><head></head><body>' const expectedEventDescription = '<html><head></head><body>'
'<a href="https://example1.com" rel="noreferrer" style="white-space: nowrap; word-break: keep-all" target="_blank">example1.com</a>'
'<br>' '<br>'
'<a href="https://example1.com" target="_blank" rel="noreferrer" style="white-space: nowrap; word-break: keep-all;">example1.com</a>' '<a href="https://example2.com" rel="noreferrer" style="white-space: nowrap; word-break: keep-all" target="_blank">example2.com</a>'
'<br>'
'<a href="https://example2.com" target="_blank" rel="noreferrer" style="white-space: nowrap; word-break: keep-all;">example2.com</a>'
'</body></html>'; '</body></html>';
final blobId = Id('abc123'); final blobId = Id('abc123');
final calendarEvent = CalendarEvent( final calendarEvent = CalendarEvent(
@@ -373,10 +372,9 @@ void main() {
'\n<script>alert(1)</script>' '\n<script>alert(1)</script>'
'\n<a href="javascript:alert(1)">href xss</a>'; '\n<a href="javascript:alert(1)">href xss</a>';
const expectedEventDescription = '<html><head></head><body>' const expectedEventDescription = '<html><head></head><body>'
'<a href="https://example1.com" rel="noreferrer" style="white-space: nowrap; word-break: keep-all" target="_blank">example1.com</a>'
'<br>' '<br>'
'<a href="https://example1.com" target="_blank" rel="noreferrer" style="white-space: nowrap; word-break: keep-all;">example1.com</a>' '<a href="https://example2.com" rel="noreferrer" style="white-space: nowrap; word-break: keep-all" target="_blank">example2.com</a>'
'<br>'
'<a href="https://example2.com" target="_blank" rel="noreferrer" style="white-space: nowrap; word-break: keep-all;">example2.com</a>'
'<br>' '<br>'
'<br>' '<br>'
'<a>href xss</a>' '<a>href xss</a>'