From 6f455cc728a16f58430ec9f8976130e5587a3ca5 Mon Sep 17 00:00:00 2001
From: dab246
Date: Mon, 15 Dec 2025 14:32:02 +0700
Subject: [PATCH] fix(mu): email content not displayed due to sanitize html
(cherry picked from commit 2f06e52d81864a62d6e588bb24a6845e20371bf4)
---
contact/pubspec.lock | 4 +-
...e_hyper_link_tag_in_html_transformers.dart | 2 +-
...ndardize_html_sanitizing_transformers.dart | 37 +-
core/pubspec.lock | 4 +-
...ize_html_sanitizing_transformers_test.dart | 421 ++++++++++++++----
.../email/data/local/html_analyzer.dart | 2 +
.../controller/single_email_controller.dart | 15 +-
model/pubspec.lock | 4 +-
pubspec.lock | 4 +-
.../single_email_controller_test.dart | 10 +-
10 files changed, 362 insertions(+), 141 deletions(-)
diff --git a/contact/pubspec.lock b/contact/pubspec.lock
index 6b769c262..6c20ded71 100644
--- a/contact/pubspec.lock
+++ b/contact/pubspec.lock
@@ -1020,10 +1020,10 @@ packages:
description:
path: sanitize_html
ref: support_mail
- resolved-ref: fda32cde4d4baadaa988477f498ab6622ee79987
+ resolved-ref: "6627e13f2236bdea5d25f648e6d55b214a4569df"
url: "https://github.com/linagora/dart-neats.git"
source: git
- version: "2.1.0"
+ version: "3.0.0"
shelf:
dependency: transitive
description:
diff --git a/core/lib/presentation/utils/html_transformer/dom/sanitize_hyper_link_tag_in_html_transformers.dart b/core/lib/presentation/utils/html_transformer/dom/sanitize_hyper_link_tag_in_html_transformers.dart
index 8211dfeb9..88712357f 100644
--- a/core/lib/presentation/utils/html_transformer/dom/sanitize_hyper_link_tag_in_html_transformers.dart
+++ b/core/lib/presentation/utils/html_transformer/dom/sanitize_hyper_link_tag_in_html_transformers.dart
@@ -16,7 +16,7 @@ class SanitizeHyperLinkTagInHtmlTransformer extends DomTransformer {
Map? mapUrlDownloadCID,
}) async {
try {
- final elements = document.querySelectorAll('a');
+ final elements = document.querySelectorAll('a[href]');
if (elements.isEmpty) return;
diff --git a/core/lib/presentation/utils/html_transformer/text/standardize_html_sanitizing_transformers.dart b/core/lib/presentation/utils/html_transformer/text/standardize_html_sanitizing_transformers.dart
index 902a62ace..06a0894ba 100644
--- a/core/lib/presentation/utils/html_transformer/text/standardize_html_sanitizing_transformers.dart
+++ b/core/lib/presentation/utils/html_transformer/text/standardize_html_sanitizing_transformers.dart
@@ -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';
class StandardizeHtmlSanitizingTransformers extends TextTransformer {
-
- static const List mailAllowedHtmlAttributes = [
- 'style',
- 'public-asset-id',
- 'data-filename',
- 'bgcolor',
- 'id',
- 'class',
- 'data-mimetype',
- ];
-
- static const List mailAllowedHtmlTags = [
- 'font',
- 'u',
- 'center',
- 'style',
- 'body',
- 'section',
- 'google-sheets-html-origin',
- 'colgroup',
- 'col',
- 'nav',
- 'main',
- 'footer',
- 'supress_time_adjustment',
- ];
+ static final SanitizeHtml _sanitizer = SanitizeHtml();
const StandardizeHtmlSanitizingTransformers();
@override
- String process(String text, HtmlEscape htmlEscape) =>
- SanitizeHtml().process(
- inputHtml: text,
- allowAttributes: mailAllowedHtmlAttributes,
- allowTags: mailAllowedHtmlTags,
- );
+ String process(String text, HtmlEscape htmlEscape) {
+ if (text.isEmpty) return '';
+ return _sanitizer.process(inputHtml: text);
+ }
}
diff --git a/core/pubspec.lock b/core/pubspec.lock
index ff2c31c8a..78bd1ab5d 100644
--- a/core/pubspec.lock
+++ b/core/pubspec.lock
@@ -973,10 +973,10 @@ packages:
description:
path: sanitize_html
ref: support_mail
- resolved-ref: fda32cde4d4baadaa988477f498ab6622ee79987
+ resolved-ref: "6627e13f2236bdea5d25f648e6d55b214a4569df"
url: "https://github.com/linagora/dart-neats.git"
source: git
- version: "2.1.0"
+ version: "3.0.0"
shelf:
dependency: transitive
description:
diff --git a/core/test/utils/standardize_html_sanitizing_transformers_test.dart b/core/test/utils/standardize_html_sanitizing_transformers_test.dart
index 48b55b13c..df325e3d3 100644
--- a/core/test/utils/standardize_html_sanitizing_transformers_test.dart
+++ b/core/test/utils/standardize_html_sanitizing_transformers_test.dart
@@ -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 'package:flutter_test/flutter_test.dart';
+import 'package:core/presentation/utils/html_transformer/text/standardize_html_sanitizing_transformers.dart';
void main() {
group('StandardizeHtmlSanitizingTransformers.process', () {
const transformer = StandardizeHtmlSanitizingTransformers();
const htmlEscape = HtmlEscape();
- const listHTMLTags = [
+
+ const allowedTags = [
'div',
'span',
'p',
@@ -16,12 +17,10 @@ void main() {
'font',
'u',
'center',
- 'style',
'section',
- 'google-sheets-html-origin',
- 'supress_time_adjustment',
];
- const listOnEventAttributes = [
+
+ const eventAttributes = [
'mousedown',
'mouseenter',
'mouseleave',
@@ -80,124 +79,368 @@ void main() {
'touchcancel',
];
- test('SHOULD remove all `on*` attributes tag', () {
- for (var i = 0; i < listOnEventAttributes.length; i++) {
- final inputHtml = '
';
- final result = transformer.process(inputHtml, htmlEscape);
+ String sanitize(String input) =>
+ transformer.process(input, htmlEscape).trim();
- expect(result, equals('
'));
- }
- });
-
- test('SHOULD remove all `on*` attributes for any tags', () {
- 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>'));
+ 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 = '
';
+ expect(sanitize(html), equals('
'));
}
- }
+ });
+
+ 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 ', () {
+ for (final evt in eventAttributes) {
+ final html = '';
+ expect(sanitize(html), equals(''));
+ }
+ });
+
+ test('SHOULD remove all on* event attributes FROM ', () {
+ for (final evt in eventAttributes) {
+ final html =
+ '';
+ expect(sanitize(html),
+ equals(''));
+ }
+ });
+
+ test('SHOULD remove '), equals(''));
+ });
+
+ test('SHOULD remove
';
+ expect(sanitize(html), equals(''));
+ });
});
- test('SHOULD remove all `on*` attributes for `colgroup` tag', () {
- for (var event in listOnEventAttributes) {
- final inputHtml = '';
- final result = transformer.process(inputHtml, htmlEscape);
+ group('Href/src sanitization – unsafe removed, safe preserved', () {
+ test('SHOULD remove javascript: href FROM ', () {
+ const html = 'test';
+ expect(sanitize(html), equals('test'));
+ });
- expect(result, equals(''));
- }
+ test('SHOULD remove invalid JS event attributes FROM
', () {
+ const html = '
';
+ expect(sanitize(html), equals('
'));
+ });
+
+ test('SHOULD keep base64 image sources intact', () {
+ const html = '
';
+ expect(
+ sanitize(html),
+ equals('
'));
+ });
+
+ test('SHOULD keep cid: image sources intact', () {
+ expect(
+ sanitize('
'),
+ equals('
'));
+ });
});
- test('SHOULD remove all `on*` attributes for `col` tag', () {
- for (var event in listOnEventAttributes) {
- final inputHtml = '';
- final result = transformer.process(inputHtml, htmlEscape);
+ group('Unknown tags, structural tags – unwrap or preserve safely', () {
+ test('SHOULD preserve nav/main/footer but strip dangerous attributes', () {
+ expect(sanitize(''), equals(''));
+ expect(sanitize(''), equals(''));
+ expect(sanitize(''), equals(''));
+ });
- expect(result, equals(''));
- }
+ test('SHOULD unwrap AND keep children', () {
+ const html =
+ '';
+ expect(sanitize(html), equals(''));
+ });
+
+ test('SHOULD unwrap AND keep children', () {
+ const html =
+ '';
+ expect(sanitize(html), equals(''));
+ });
+
+ test('SHOULD unwrap unknown tags AND keep children', () {
+ expect(sanitize('Hello
'), equals('Hello
'));
+ });
+
+ test('SHOULD unwrap nested unknown tags AND keep children', () {
+ expect(
+ sanitize('Hello
'),
+ equals('Hello
'));
+ });
});
- test('SHOULD remove attributes of IMG tag WHEN they are invalid', () {
- const inputHtml = '
';
- final result = transformer.process(inputHtml, htmlEscape);
+ group('CSS sanitization – unsafe styles removed', () {
+ test('SHOULD keep plain text containing JS-like keywords', () {
+ const html = 'hello document.cookie
world';
- expect(result, equals('
'));
+ expect(
+ sanitize(html),
+ equals('hello document.cookie
world'),
+ );
+ });
+
+ test('SHOULD remove HTML comments entirely', () {
+ const html = '';
+ expect(sanitize(html), equals(''));
+ });
+
+ test('SHOULD remove unsafe CSS FROM ';
+ expect(sanitize(html), equals(''));
+ });
+
+ test('SHOULD sanitize inline CSS by removing dangerous properties', () {
+ const html =
+ 'X
';
+ expect(sanitize(html), equals('X
'));
+ });
});
- test('SHOULD remove all SCRIPTS tags', () {
- const inputHtml = '';
- final result = transformer.process(inputHtml, htmlEscape).trim();
+ group('ID and class validation – valid preserved, invalid removed', () {
+ test('SHOULD preserve valid id/class values', () {
+ const html = '';
+ expect(
+ sanitize(html),
+ equals(''));
+ });
- expect(result, equals(''));
+ test('SHOULD remove invalid id/class values', () {
+ const html = '';
+ expect(sanitize(html), equals(''));
+ });
});
- test('SHOULD remove all IFRAME tags', () {
- const inputHtml = '