diff --git a/sanitize_html/lib/sanitize_html.dart b/sanitize_html/lib/sanitize_html.dart
index dc298d48..fb4a6134 100644
--- a/sanitize_html/lib/sanitize_html.dart
+++ b/sanitize_html/lib/sanitize_html.dart
@@ -76,10 +76,14 @@ String sanitizeHtml(
bool Function(String)? allowElementId,
bool Function(String)? allowClassName,
Iterable? Function(String)? addLinkRel,
+ List? allowAttributes,
+ List? allowTags,
}) {
return SaneHtmlValidator(
allowElementId: allowElementId,
allowClassName: allowClassName,
addLinkRel: addLinkRel,
+ allowAttributes: allowAttributes,
+ allowTags: allowTags,
).sanitize(htmlString);
}
diff --git a/sanitize_html/lib/src/sane_html_validator.dart b/sanitize_html/lib/src/sane_html_validator.dart
index 32e39a81..caae2821 100644
--- a/sanitize_html/lib/src/sane_html_validator.dart
+++ b/sanitize_html/lib/src/sane_html_validator.dart
@@ -14,6 +14,7 @@
import 'package:html/dom.dart';
import 'package:html/parser.dart' as html_parser;
+import 'package:meta/meta.dart';
final _allowedElements = {
'H1',
@@ -177,6 +178,33 @@ bool _validUrl(String url) {
}
}
+bool _validBase64Image(String base64String) {
+ try {
+ final regex = RegExp(r'^data:image\/(png|jpeg|jpg|gif|bmp|svg\+xml);base64,[A-Za-z0-9+/]+={0,2}$');
+ return regex.hasMatch(base64String);
+ } catch (e) {
+ return false;
+ }
+}
+
+bool _validCIDImage(String cidString) {
+ try {
+ return cidString.startsWith('cid:');
+ } catch (e) {
+ return false;
+ }
+}
+
+@visibleForTesting
+bool validateBase64Image(String base64String) => _validBase64Image(base64String);
+
+@visibleForTesting
+bool validateCIDImage(String cidString) => _validCIDImage(cidString);
+
+bool _validImageSource(String url) {
+ return _validUrl(url) || _validBase64Image(url) || validateCIDImage(url);
+}
+
final _citeAttributeValidator = {
'cite': _validUrl,
};
@@ -187,8 +215,8 @@ final _elementAttributeValidators =
'href': _validLink,
},
'IMG': {
- 'src': _validUrl,
- 'longdesc': _validUrl,
+ 'src': _validImageSource,
+ 'longdesc': _validImageSource,
},
'DIV': {
'itemscope': _alwaysAllowed,
@@ -212,11 +240,15 @@ class SaneHtmlValidator {
final bool Function(String)? allowElementId;
final bool Function(String)? allowClassName;
final Iterable? Function(String)? addLinkRel;
+ final List? allowAttributes;
+ final List? allowTags;
SaneHtmlValidator({
required this.allowElementId,
required this.allowClassName,
required this.addLinkRel,
+ required this.allowAttributes,
+ required this.allowTags,
});
String sanitize(String htmlString) {
@@ -228,16 +260,19 @@ class SaneHtmlValidator {
void _sanitize(Node node) {
if (node is Element) {
final tagName = node.localName!.toUpperCase();
- if (!_allowedElements.contains(tagName)) {
+ if (!_allowedElements.contains(tagName)
+ && !(allowTags?.contains(tagName.toLowerCase()) ?? false)) {
node.remove();
return;
}
node.attributes.removeWhere((k, v) {
final attrName = k.toString();
if (attrName == 'id') {
- return allowElementId == null || !allowElementId!(v);
+ return allowAttributes?.contains('id') != true &&
+ (allowElementId == null || !allowElementId!(v));
}
if (attrName == 'class') {
+ if (allowAttributes?.contains('class') == true) return false;
if (allowClassName == null) return true;
node.classes.removeWhere((cn) => !allowClassName!(cn));
return node.classes.isEmpty;
@@ -269,6 +304,8 @@ class SaneHtmlValidator {
}
bool _isAttributeAllowed(String tagName, String attrName, String value) {
+ if (allowAttributes?.contains(attrName.toLowerCase()) == true) return true;
+
if (_alwaysAllowedAttributes.contains(attrName)) return true;
// Special validators for special attributes on special tags (href/src/cite)
diff --git a/sanitize_html/test/validate_base64_image_test.dart b/sanitize_html/test/validate_base64_image_test.dart
new file mode 100644
index 00000000..33d34271
--- /dev/null
+++ b/sanitize_html/test/validate_base64_image_test.dart
@@ -0,0 +1,46 @@
+import 'package:sanitize_html/src/sane_html_validator.dart';
+import 'package:test/test.dart';
+
+void main() {
+ group('validateBase64Image', () {
+ test('Valid Base64 PNG image string', () {
+ String validBase64PNG = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA';
+ expect(validateBase64Image(validBase64PNG), isTrue);
+ });
+
+ test('Valid Base64 JPEG image string', () {
+ String validBase64JPEG = 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAAAAAA';
+ expect(validateBase64Image(validBase64JPEG), isTrue);
+ });
+
+ test('Invalid Base64 image string (missing data:image/)', () {
+ String invalidBase64 = 'base64,iVBORw0KGgoAAAANSUhEUgAAAAUA';
+ expect(validateBase64Image(invalidBase64), isFalse);
+ });
+
+ test('Invalid Base64 image string (not base64 encoded)', () {
+ String invalidBase64 = 'data:image/png;notabase64string';
+ expect(validateBase64Image(invalidBase64), isFalse);
+ });
+
+ test('Valid Base64 SVG image string', () {
+ String validBase64SVG = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDov';
+ expect(validateBase64Image(validBase64SVG), isTrue);
+ });
+
+ test('Invalid Base64 image string (wrong image type)', () {
+ String invalidBase64Type = 'data:image/tiff;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA';
+ expect(validateBase64Image(invalidBase64Type), isFalse);
+ });
+
+ test('Empty string', () {
+ String emptyString = '';
+ expect(validateBase64Image(emptyString), isFalse);
+ });
+
+ test('Non-image Base64 string', () {
+ String nonImageBase64 = 'data:text/plain;base64,dGVzdA=='; // Plain text Base64-encoded
+ expect(validateBase64Image(nonImageBase64), isFalse);
+ });
+ });
+}
diff --git a/sanitize_html/test/validate_cid_image_test.dart b/sanitize_html/test/validate_cid_image_test.dart
new file mode 100644
index 00000000..8cc0879a
--- /dev/null
+++ b/sanitize_html/test/validate_cid_image_test.dart
@@ -0,0 +1,18 @@
+import 'package:sanitize_html/src/sane_html_validator.dart';
+import 'package:test/test.dart';
+
+void main() {
+ group('validateCIDImage', () {
+ test('returns true for valid cid string', () {
+ expect(validateCIDImage('cid:12345'), true);
+ });
+
+ test('returns false for string without cid', () {
+ expect(validateCIDImage('https://example.com/image.png'), false);
+ });
+
+ test('returns false for empty string', () {
+ expect(validateCIDImage(''), false);
+ });
+ });
+}