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)">'; - final result = transformer.process(inputHtml, htmlEscape); - - expect(result, equals('<$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)">'; + expect(sanitize(html), equals('<$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 '), equals('')); + }); + + test('SHOULD remove nested

'; + expect(sanitize(html), equals('

Hello

')); + }); }); - 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 = + '

Hello

'; + expect(sanitize(html), equals('

Hello

')); + }); + + test('SHOULD unwrap AND keep children', () { + const html = + '

Hello

'; + expect(sanitize(html), equals('

Hello

')); + }); + + 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 = '

A

'; + expect(sanitize(html), equals('

A

')); + }); + + 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 = ' + + '''; + + 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); + }); }); }); } diff --git a/lib/features/email/data/local/html_analyzer.dart b/lib/features/email/data/local/html_analyzer.dart index cfa5618db..03898d90c 100644 --- a/lib/features/email/data/local/html_analyzer.dart +++ b/lib/features/email/data/local/html_analyzer.dart @@ -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/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/standardize_html_sanitizing_transformers.dart'; import 'package:core/presentation/utils/html_transformer/transform_configuration.dart'; import 'package:core/utils/app_logger.dart'; import 'package:core/utils/string_convert.dart'; @@ -48,6 +49,7 @@ class HtmlAnalyzer { final message = _htmlTransform.transformToTextPlain( content: emailContent.content, transformConfiguration: TransformConfiguration.fromTextTransformers([ + const StandardizeHtmlSanitizingTransformers(), const SanitizeAutolinkHtmlTransformers(), const PersistPreformattedTextTransformer(), ]), diff --git a/lib/features/email/presentation/controller/single_email_controller.dart b/lib/features/email/presentation/controller/single_email_controller.dart index 20f459986..4a9cffda1 100644 --- a/lib/features/email/presentation/controller/single_email_controller.dart +++ b/lib/features/email/presentation/controller/single_email_controller.dart @@ -1175,11 +1175,16 @@ class SingleEmailController extends BaseController with AppLoaderMixin { consumeState(_parseCalendarEventInteractor!.execute( accountId, blobIds, - TransformConfiguration.fromTextTransformers(const [ - SanitizeAutolinkUnescapeHtmlTransformer(), - StandardizeHtmlSanitizingTransformers(), - NewLineTransformer(), - ]) + TransformConfiguration.create( + customTextTransformers: const [ + SanitizeAutolinkUnescapeHtmlTransformer(), + StandardizeHtmlSanitizingTransformers(), + NewLineTransformer(), + ], + customDomTransformers: [ + SanitizeHyperLinkTagInHtmlTransformer(), + ] + ) )); } diff --git a/model/pubspec.lock b/model/pubspec.lock index 39417484d..285ba585b 100644 --- a/model/pubspec.lock +++ b/model/pubspec.lock @@ -997,10 +997,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/pubspec.lock b/pubspec.lock index 7814e15a5..9c9ccfe60 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1997,10 +1997,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" scribe: dependency: "direct main" description: diff --git a/test/features/email/presentation/controller/single_email_controller_test.dart b/test/features/email/presentation/controller/single_email_controller_test.dart index 2628016af..a2b715e50 100644 --- a/test/features/email/presentation/controller/single_email_controller_test.dart +++ b/test/features/email/presentation/controller/single_email_controller_test.dart @@ -316,10 +316,9 @@ void main() { // arrange const eventDescription = '\nhttps://example1.com\nhttps://example2.com'; const expectedEventDescription = '' + 'example1.com' '
' - 'example1.com' - '
' - 'example2.com' + 'example2.com' ''; final blobId = Id('abc123'); final calendarEvent = CalendarEvent( @@ -373,10 +372,9 @@ void main() { '\n' '\nhref xss'; const expectedEventDescription = '' + 'example1.com' '
' - 'example1.com' - '
' - 'example2.com' + 'example2.com' '
' '
' 'href xss'