Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Standardize HTML sanitizing when preview email #3223

Merged
merged 9 commits into from
Oct 24, 2024
9 changes: 9 additions & 0 deletions contact/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -992,6 +992,15 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.2.1"
sanitize_html:
dab246 marked this conversation as resolved.
Show resolved Hide resolved
dependency: transitive
description:
path: sanitize_html
ref: support_mail
resolved-ref: fda32cde4d4baadaa988477f498ab6622ee79987
url: "https://github.com/linagora/dart-neats.git"
source: git
version: "2.1.0"
shelf:
dependency: transitive
description:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,12 @@ class MessageContentTransformer {
Map<String, String>? mapUrlDownloadCID
}) async {
await Future.wait([
if (_configuration.domTransformers.isNotEmpty)
..._configuration.domTransformers.map((domTransformer) async =>
domTransformer.process(
document: document,
dioClient: _dioClient,
mapUrlDownloadCID: mapUrlDownloadCID,
)
..._configuration.domTransformers.map((domTransformer) async =>
domTransformer.process(
document: document,
dioClient: _dioClient,
mapUrlDownloadCID: mapUrlDownloadCID,
)
)
]);
}
Expand All @@ -38,24 +37,32 @@ class MessageContentTransformer {
required String message,
Map<String, String>? mapUrlDownloadCID
}) async {
final document = parse(message);
await _transformDocument(
document: document,
mapUrlDownloadCID: mapUrlDownloadCID,
);
final newMessage = _configuration.textTransformers.isNotEmpty
? _transformMessage(message)
: message;

final document = parse(newMessage);

if (_configuration.domTransformers.isNotEmpty) {
await _transformDocument(
document: document,
mapUrlDownloadCID: mapUrlDownloadCID,
);
}

return document;
}

String _transformMessage(String message) {
if (_configuration.textTransformers.isNotEmpty) {
for (var transformer in _configuration.textTransformers) {
message = transformer.process(message, _htmlEscape);
}
for (var transformer in _configuration.textTransformers) {
message = transformer.process(message, _htmlEscape);
}
return message;
}

String toMessage(String message) {
return _transformMessage(message);
return _configuration.textTransformers.isNotEmpty
? _transformMessage(message)
: message;
}
}
16 changes: 16 additions & 0 deletions core/lib/presentation/utils/html_transformer/sanitize_html.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import 'package:sanitize_html/sanitize_html.dart';

class SanitizeHtml {
String process({
required String inputHtml,
List<String>? allowAttributes,
List<String>? allowTags,
}) {
final outputHtml = sanitizeHtml(
inputHtml,
allowAttributes: allowAttributes,
allowTags: allowTags,
);
return outputHtml;
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import 'package:core/utils/app_logger.dart';
import 'package:get/get.dart';

class SanitizeUrl {
Expand All @@ -24,7 +23,6 @@ class SanitizeUrl {
} else {
originalUrl = '';
}
log('SanitizeUrl::process:originalUrl = $originalUrl');
return originalUrl;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import 'dart:convert';
import 'package:core/presentation/utils/html_transformer/base/text_transformer.dart';
import 'package:core/presentation/utils/html_transformer/sanitize_html.dart';

class StandardizeHtmlSanitizingTransformers extends TextTransformer {

static const List<String> mailAllowedHtmlAttributes = [
'style',
'public-asset-id',
'data-filename',
'bgcolor',
'id',
'class',
];

static const List<String> mailAllowedHtmlTags = [
'font',
'u',
'center',
'style',
'body',
];

const StandardizeHtmlSanitizingTransformers();

@override
String process(String text, HtmlEscape htmlEscape) =>
SanitizeHtml().process(
hoangdat marked this conversation as resolved.
Show resolved Hide resolved
inputHtml: text,
allowAttributes: mailAllowedHtmlAttributes,
allowTags: mailAllowedHtmlTags,
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import 'package:core/presentation/utils/html_transformer/dom/remove_tooltip_link
import 'package:core/presentation/utils/html_transformer/dom/sanitize_hyper_link_tag_in_html_transformers.dart';
import 'package:core/presentation/utils/html_transformer/dom/script_transformers.dart';
import 'package:core/presentation/utils/html_transformer/dom/signature_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/utils/platform_info.dart';

/// Contains the configuration for all transformations.
Expand All @@ -37,7 +37,9 @@ class TransformConfiguration {

factory TransformConfiguration.fromDomTransformers(List<DomTransformer> domTransformers) => TransformConfiguration(domTransformers, []);

factory TransformConfiguration.empty() => const TransformConfiguration([], []);
factory TransformConfiguration.fromTextTransformers(
List<TextTransformer> textTransformers
) => TransformConfiguration([], textTransformers);

factory TransformConfiguration.forReplyForwardEmail() => TransformConfiguration.fromDomTransformers([
if (PlatformInfo.isWeb)
Expand All @@ -46,10 +48,15 @@ class TransformConfiguration {
const RemoveCollapsedSignatureButtonTransformer(),
]);

factory TransformConfiguration.forDraftsEmail() => TransformConfiguration.fromDomTransformers([const ImageTransformer()]);
factory TransformConfiguration.forEditDraftsEmail() => TransformConfiguration.fromDomTransformers([
...TransformConfiguration.forDraftsEmail().domTransformers,
const HideDraftSignatureTransformer()]);
factory TransformConfiguration.forDraftsEmail() => TransformConfiguration.create(
customDomTransformers: [const ImageTransformer()]
);
factory TransformConfiguration.forEditDraftsEmail() => TransformConfiguration.create(
customDomTransformers: [
...TransformConfiguration.forDraftsEmail().domTransformers,
const HideDraftSignatureTransformer()
]
);

factory TransformConfiguration.forPreviewEmailOnWeb() => TransformConfiguration.create(
customDomTransformers: [
Expand All @@ -65,7 +72,9 @@ class TransformConfiguration {

factory TransformConfiguration.forPreviewEmail() => TransformConfiguration.standardConfiguration;

factory TransformConfiguration.forRestoreEmail() => TransformConfiguration.fromDomTransformers([const ImageTransformer()]);
factory TransformConfiguration.forRestoreEmail() => TransformConfiguration.create(
customDomTransformers: [const ImageTransformer()]
);

factory TransformConfiguration.forPrintEmail() => TransformConfiguration.fromDomTransformers([
if (PlatformInfo.isWeb)
Expand Down Expand Up @@ -115,6 +124,6 @@ class TransformConfiguration {
];

static const List<TextTransformer> standardTextTransformers = [
SanitizeAutolinkHtmlTransformers()
StandardizeHtmlSanitizingTransformers(),
];
}
9 changes: 9 additions & 0 deletions core/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -945,6 +945,15 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.0.1"
sanitize_html:
dependency: "direct main"
description:
path: sanitize_html
ref: support_mail
resolved-ref: fda32cde4d4baadaa988477f498ab6622ee79987
url: "https://github.com/linagora/dart-neats.git"
source: git
version: "2.1.0"
shelf:
dependency: transitive
description:
Expand Down
9 changes: 9 additions & 0 deletions core/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,15 @@ dependencies:
url: https://github.com/dab246/languagetool_textfield.git
ref: twake-supported

# Sanitize_html is restricting Tags and Attributes. So some of our own tags and attributes (signature, public asset,...) will be lost when sanitizing html.
# TODO: We will change it when the PR in upstream repository will be merged
# https://github.com/google/dart-neats/pull/259
dab246 marked this conversation as resolved.
Show resolved Hide resolved
sanitize_html:
git:
url: https://github.com/linagora/dart-neats.git
ref: support_mail
path: sanitize_html

### Dependencies from pub.dev ###
cupertino_icons: 1.0.6

Expand Down
147 changes: 147 additions & 0 deletions core/test/utils/standardize_html_sanitizing_transformers_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import 'package:core/presentation/utils/html_transformer/text/standardize_html_sanitizing_transformers.dart';
import 'package:flutter_test/flutter_test.dart';
import 'dart:convert';

void main() {
group('StandardizeHtmlSanitizingTransformers::test', () {
const transformer = StandardizeHtmlSanitizingTransformers();
const htmlEscape = HtmlEscape();

test('SHOULD remove all `on*` attributes tag', () {
const listOnEventAttributes = [
'mousedown',
'mouseenter',
'mouseleave',
'mousemove',
'mouseover',
'mouseout',
'mouseup',
'load',
'unload',
'loadstart',
'loadeddata',
'loadedmetadata',
'playing',
'show',
'error',
'message',
'focus',
'focusin',
'focusout',
'keydown',
'keydpress',
'keydup',
'input',
'ended',
'drag',
'drop',
'dragstart',
'dragover',
'dragleave',
'dragend',
'dragenter',
'beforeunload',
'beforeprint',
'afterprint',
'blur',
'click',
'change',
'contextmenu',
'cut',
'copy',
'dblclick',
'abort',
'durationchange',
'progress',
'resize',
'reset',
'scroll',
'seeked',
'select',
'submit',
'toggle',
'volumechange',
'touchstart',
'touchmove',
'touchend',
'touchcancel'
];

for (var i = 0; i < listOnEventAttributes.length; i++) {
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">'));
}
});

test('SHOULD remove all `on*` attributes for any tags', () {
const listOnEventAttributes = [
'mousedown', 'mouseenter', 'mouseleave', 'mousemove', 'mouseover',
'mouseout', 'mouseup', 'load', 'unload', 'loadstart', 'loadeddata',
'loadedmetadata', 'playing', 'show', 'error', 'message', 'focus',
'focusin', 'focusout', 'keydown', 'keypress', 'keyup', 'input', 'ended',
'drag', 'drop', 'dragstart', 'dragover', 'dragleave', 'dragend', 'dragenter',
'beforeunload', 'beforeprint', 'afterprint', 'blur', 'click', 'change',
'contextmenu', 'cut', 'copy', 'dblclick', 'abort', 'durationchange',
'progress', 'resize', 'reset', 'scroll', 'seeked', 'select', 'submit',
'toggle', 'volumechange', 'touchstart', 'touchmove', 'touchend', 'touchcancel'
];

const listHTMLTags = [
'div', 'span', 'p', 'a', 'u', 'i', 'table'
];

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 attributes of IMG tag WHEN they are invalid', () {
const inputHtml = '<img src="1" href="1" onerror="javascript:alert(1)">';
hoangdat marked this conversation as resolved.
Show resolved Hide resolved
final result = transformer.process(inputHtml, htmlEscape);

expect(result, equals('<img src="1">'));
});

test('SHOULD remove all SCRIPTS tags', () {
const inputHtml = '<script>alert("This is an alert message!");</script>';
final result = transformer.process(inputHtml, htmlEscape).trim();

expect(result, equals(''));
});

test('SHOULD remove all IFRAME tags', () {
const inputHtml = '<iframe style="xg-p:absolute;top:0;left:0;width:100%;height:100%" onmouseover="prompt(1)">';
final result = transformer.process(inputHtml, htmlEscape);

expect(result, equals(''));
});

test('SHOULD remove href attribute of A tag WHEN it is invalid', () {
const inputHtml = '<a href="javascript:alert(1)" id="id1">test</a>';
final result = transformer.process(inputHtml, htmlEscape);

expect(result, equals('<a id="id1">test</a>'));
});

test('SHOULD persist value src attribute of IMG tag WHEN it is base64 string', () {
const inputHtml = '<img src="data:image/jpeg;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA">';
final result = transformer.process(inputHtml, htmlEscape);

expect(result, equals('<img src="data:image/jpeg;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA">'));
});

test('SHOULD persist value src attribute of IMG tag WHEN it is CID string', () {
const inputHtml = '<img src="cid:email123">';
final result = transformer.process(inputHtml, htmlEscape);

expect(result, equals('<img src="cid:email123">'));
});
});
}
Loading
Loading