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

Additional allow attributes & tags #259

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions sanitize_html/lib/sanitize_html.dart
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,14 @@ String sanitizeHtml(
bool Function(String)? allowElementId,
bool Function(String)? allowClassName,
Iterable<String>? Function(String)? addLinkRel,
List<String>? allowAttributes,
List<String>? allowTags,
}) {
return SaneHtmlValidator(
allowElementId: allowElementId,
allowClassName: allowClassName,
addLinkRel: addLinkRel,
allowAttributes: allowAttributes,
allowTags: allowTags,
).sanitize(htmlString);
}
45 changes: 41 additions & 4 deletions sanitize_html/lib/src/sane_html_validator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

import 'package:html/dom.dart';
import 'package:html/parser.dart' as html_parser;
import 'package:meta/meta.dart';

final _allowedElements = <String>{
'H1',
Expand Down Expand Up @@ -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 = <String, bool Function(String)>{
'cite': _validUrl,
};
Expand All @@ -187,8 +215,8 @@ final _elementAttributeValidators =
'href': _validLink,
},
'IMG': {
'src': _validUrl,
'longdesc': _validUrl,
'src': _validImageSource,
'longdesc': _validImageSource,
},
'DIV': {
'itemscope': _alwaysAllowed,
Expand All @@ -212,11 +240,15 @@ class SaneHtmlValidator {
final bool Function(String)? allowElementId;
final bool Function(String)? allowClassName;
final Iterable<String>? Function(String)? addLinkRel;
final List<String>? allowAttributes;
final List<String>? allowTags;

SaneHtmlValidator({
required this.allowElementId,
required this.allowClassName,
required this.addLinkRel,
required this.allowAttributes,
required this.allowTags,
});

String sanitize(String htmlString) {
Expand All @@ -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;
Expand Down Expand Up @@ -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)
Expand Down
46 changes: 46 additions & 0 deletions sanitize_html/test/validate_base64_image_test.dart
Original file line number Diff line number Diff line change
@@ -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);
});
});
}
18 changes: 18 additions & 0 deletions sanitize_html/test/validate_cid_image_test.dart
Original file line number Diff line number Diff line change
@@ -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);
});
});
}