fix(mu): email content not displayed due to sanitize html
(cherry picked from commit 2f06e52d81864a62d6e588bb24a6845e20371bf4)
This commit is contained in:
@@ -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:
|
||||||
|
|||||||
+1
-1
@@ -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;
|
||||||
|
|
||||||
|
|||||||
+5
-32
@@ -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
@@ -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
@@ -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
@@ -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>'
|
||||||
|
|||||||
Reference in New Issue
Block a user