| // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file | |
| // for details. All rights reserved. Use of this source code is governed by a | |
| // BSD-style license that can be found in the LICENSE file. | |
| part of dart.dom.html; | |
| /** | |
| * Class which helps construct standard node validation policies. | |
| * | |
| * By default this will not accept anything, but the 'allow*' functions can be | |
| * used to expand what types of elements or attributes are allowed. | |
| * | |
| * All allow functions are additive- elements will be accepted if they are | |
| * accepted by any specific rule. | |
| * | |
| * It is important to remember that sanitization is not just intended to prevent | |
| * cross-site scripting attacks, but also to prevent information from being | |
| * displayed in unexpected ways. For example something displaying basic | |
| * formatted text may not expect `<video>` tags to appear. In this case an | |
| * empty NodeValidatorBuilder with just [allowTextElements] might be | |
| * appropriate. | |
| */ | |
| class NodeValidatorBuilder implements NodeValidator { | |
| final List<NodeValidator> _validators = <NodeValidator>[]; | |
| NodeValidatorBuilder() {} | |
| /** | |
| * Creates a new NodeValidatorBuilder which accepts common constructs. | |
| * | |
| * By default this will accept HTML5 elements and attributes with the default | |
| * [UriPolicy] and templating elements. | |
| * | |
| * Notable syntax which is filtered: | |
| * | |
| * * Only known-good HTML5 elements and attributes are allowed. | |
| * * All URLs must be same-origin, use [allowNavigation] and [allowImages] to | |
| * specify additional URI policies. | |
| * * Inline-styles are not allowed. | |
| * * Custom element tags are disallowed, use [allowCustomElement]. | |
| * * Custom tags extensions are disallowed, use [allowTagExtension]. | |
| * * SVG Elements are not allowed, use [allowSvg]. | |
| * | |
| * For scenarios where the HTML should only contain formatted text | |
| * [allowTextElements] is more appropriate. | |
| * | |
| * Use [allowSvg] to allow SVG elements. | |
| */ | |
| NodeValidatorBuilder.common() { | |
| allowHtml5(); | |
| allowTemplating(); | |
| } | |
| /** | |
| * Allows navigation elements- Form and Anchor tags, along with common | |
| * attributes. | |
| * | |
| * The UriPolicy can be used to restrict the locations the navigation elements | |
| * are allowed to direct to. By default this will use the default [UriPolicy]. | |
| */ | |
| void allowNavigation([UriPolicy? uriPolicy]) { | |
| if (uriPolicy == null) { | |
| uriPolicy = new UriPolicy(); | |
| } | |
| add(new _SimpleNodeValidator.allowNavigation(uriPolicy)); | |
| } | |
| /** | |
| * Allows image elements. | |
| * | |
| * The UriPolicy can be used to restrict the locations the images may be | |
| * loaded from. By default this will use the default [UriPolicy]. | |
| */ | |
| void allowImages([UriPolicy? uriPolicy]) { | |
| if (uriPolicy == null) { | |
| uriPolicy = new UriPolicy(); | |
| } | |
| add(new _SimpleNodeValidator.allowImages(uriPolicy)); | |
| } | |
| /** | |
| * Allow basic text elements. | |
| * | |
| * This allows a subset of HTML5 elements, specifically just these tags and | |
| * no attributes. | |
| * | |
| * * B | |
| * * BLOCKQUOTE | |
| * * BR | |
| * * EM | |
| * * H1 | |
| * * H2 | |
| * * H3 | |
| * * H4 | |
| * * H5 | |
| * * H6 | |
| * * HR | |
| * * I | |
| * * LI | |
| * * OL | |
| * * P | |
| * * SPAN | |
| * * UL | |
| */ | |
| void allowTextElements() { | |
| add(new _SimpleNodeValidator.allowTextElements()); | |
| } | |
| /** | |
| * Allow inline styles on elements. | |
| * | |
| * If [tagName] is not specified then this allows inline styles on all | |
| * elements. Otherwise tagName limits the styles to the specified elements. | |
| */ | |
| void allowInlineStyles({String? tagName}) { | |
| if (tagName == null) { | |
| tagName = '*'; | |
| } else { | |
| tagName = tagName.toUpperCase(); | |
| } | |
| add(new _SimpleNodeValidator(null, allowedAttributes: ['$tagName::style'])); | |
| } | |
| /** | |
| * Allow common safe HTML5 elements and attributes. | |
| * | |
| * This list is based off of the Caja whitelists at: | |
| * https://code.google.com/p/google-caja/wiki/CajaWhitelists. | |
| * | |
| * Common things which are not allowed are script elements, style attributes | |
| * and any script handlers. | |
| */ | |
| void allowHtml5({UriPolicy? uriPolicy}) { | |
| add(new _Html5NodeValidator(uriPolicy: uriPolicy)); | |
| } | |
| /** | |
| * Allow SVG elements and attributes except for known bad ones. | |
| */ | |
| void allowSvg() { | |
| add(new _SvgNodeValidator()); | |
| } | |
| /** | |
| * Allow custom elements with the specified tag name and specified attributes. | |
| * | |
| * This will allow the elements as custom tags (such as <x-foo></x-foo>), | |
| * but will not allow tag extensions. Use [allowTagExtension] to allow | |
| * tag extensions. | |
| */ | |
| void allowCustomElement(String tagName, | |
| {UriPolicy? uriPolicy, | |
| Iterable<String>? attributes, | |
| Iterable<String>? uriAttributes}) { | |
| var tagNameUpper = tagName.toUpperCase(); | |
| var attrs = attributes | |
| ?.map<String>((name) => '$tagNameUpper::${name.toLowerCase()}'); | |
| var uriAttrs = uriAttributes | |
| ?.map<String>((name) => '$tagNameUpper::${name.toLowerCase()}'); | |
| if (uriPolicy == null) { | |
| uriPolicy = new UriPolicy(); | |
| } | |
| add(new _CustomElementNodeValidator( | |
| uriPolicy, [tagNameUpper], attrs, uriAttrs, false, true)); | |
| } | |
| /** | |
| * Allow custom tag extensions with the specified type name and specified | |
| * attributes. | |
| * | |
| * This will allow tag extensions (such as <div is="x-foo"></div>), | |
| * but will not allow custom tags. Use [allowCustomElement] to allow | |
| * custom tags. | |
| */ | |
| void allowTagExtension(String tagName, String baseName, | |
| {UriPolicy? uriPolicy, | |
| Iterable<String>? attributes, | |
| Iterable<String>? uriAttributes}) { | |
| var baseNameUpper = baseName.toUpperCase(); | |
| var tagNameUpper = tagName.toUpperCase(); | |
| var attrs = attributes | |
| ?.map<String>((name) => '$baseNameUpper::${name.toLowerCase()}'); | |
| var uriAttrs = uriAttributes | |
| ?.map<String>((name) => '$baseNameUpper::${name.toLowerCase()}'); | |
| if (uriPolicy == null) { | |
| uriPolicy = new UriPolicy(); | |
| } | |
| add(new _CustomElementNodeValidator(uriPolicy, | |
| [tagNameUpper, baseNameUpper], attrs, uriAttrs, true, false)); | |
| } | |
| void allowElement(String tagName, | |
| {UriPolicy? uriPolicy, | |
| Iterable<String>? attributes, | |
| Iterable<String>? uriAttributes}) { | |
| allowCustomElement(tagName, | |
| uriPolicy: uriPolicy, | |
| attributes: attributes, | |
| uriAttributes: uriAttributes); | |
| } | |
| /** | |
| * Allow templating elements (such as <template> and template-related | |
| * attributes. | |
| * | |
| * This still requires other validators to allow regular attributes to be | |
| * bound (such as [allowHtml5]). | |
| */ | |
| void allowTemplating() { | |
| add(new _TemplatingNodeValidator()); | |
| } | |
| /** | |
| * Add an additional validator to the current list of validators. | |
| * | |
| * Elements and attributes will be accepted if they are accepted by any | |
| * validators. | |
| */ | |
| void add(NodeValidator validator) { | |
| _validators.add(validator); | |
| } | |
| bool allowsElement(Element element) { | |
| return _validators.any((v) => v.allowsElement(element)); | |
| } | |
| bool allowsAttribute(Element element, String attributeName, String value) { | |
| return _validators | |
| .any((v) => v.allowsAttribute(element, attributeName, value)); | |
| } | |
| } | |
| class _SimpleNodeValidator implements NodeValidator { | |
| final Set<String> allowedElements = new Set<String>(); | |
| final Set<String> allowedAttributes = new Set<String>(); | |
| final Set<String> allowedUriAttributes = new Set<String>(); | |
| final UriPolicy? uriPolicy; | |
| factory _SimpleNodeValidator.allowNavigation(UriPolicy uriPolicy) { | |
| return new _SimpleNodeValidator(uriPolicy, allowedElements: const [ | |
| 'A', | |
| 'FORM' | |
| ], allowedAttributes: const [ | |
| 'A::accesskey', | |
| 'A::coords', | |
| 'A::hreflang', | |
| 'A::name', | |
| 'A::shape', | |
| 'A::tabindex', | |
| 'A::target', | |
| 'A::type', | |
| 'FORM::accept', | |
| 'FORM::autocomplete', | |
| 'FORM::enctype', | |
| 'FORM::method', | |
| 'FORM::name', | |
| 'FORM::novalidate', | |
| 'FORM::target', | |
| ], allowedUriAttributes: const [ | |
| 'A::href', | |
| 'FORM::action', | |
| ]); | |
| } | |
| factory _SimpleNodeValidator.allowImages(UriPolicy uriPolicy) { | |
| return new _SimpleNodeValidator(uriPolicy, allowedElements: const [ | |
| 'IMG' | |
| ], allowedAttributes: const [ | |
| 'IMG::align', | |
| 'IMG::alt', | |
| 'IMG::border', | |
| 'IMG::height', | |
| 'IMG::hspace', | |
| 'IMG::ismap', | |
| 'IMG::name', | |
| 'IMG::usemap', | |
| 'IMG::vspace', | |
| 'IMG::width', | |
| ], allowedUriAttributes: const [ | |
| 'IMG::src', | |
| ]); | |
| } | |
| factory _SimpleNodeValidator.allowTextElements() { | |
| return new _SimpleNodeValidator(null, allowedElements: const [ | |
| 'B', | |
| 'BLOCKQUOTE', | |
| 'BR', | |
| 'EM', | |
| 'H1', | |
| 'H2', | |
| 'H3', | |
| 'H4', | |
| 'H5', | |
| 'H6', | |
| 'HR', | |
| 'I', | |
| 'LI', | |
| 'OL', | |
| 'P', | |
| 'SPAN', | |
| 'UL', | |
| ]); | |
| } | |
| /** | |
| * Elements must be uppercased tag names. For example `'IMG'`. | |
| * Attributes must be uppercased tag name followed by :: followed by | |
| * lowercase attribute name. For example `'IMG:src'`. | |
| */ | |
| _SimpleNodeValidator(this.uriPolicy, | |
| {Iterable<String>? allowedElements, | |
| Iterable<String>? allowedAttributes, | |
| Iterable<String>? allowedUriAttributes}) { | |
| this.allowedElements.addAll(allowedElements ?? const []); | |
| allowedAttributes = allowedAttributes ?? const []; | |
| allowedUriAttributes = allowedUriAttributes ?? const []; | |
| var legalAttributes = allowedAttributes | |
| .where((x) => !_Html5NodeValidator._uriAttributes.contains(x)); | |
| var extraUriAttributes = allowedAttributes | |
| .where((x) => _Html5NodeValidator._uriAttributes.contains(x)); | |
| this.allowedAttributes.addAll(legalAttributes); | |
| this.allowedUriAttributes.addAll(allowedUriAttributes); | |
| this.allowedUriAttributes.addAll(extraUriAttributes); | |
| } | |
| bool allowsElement(Element element) { | |
| return allowedElements.contains(Element._safeTagName(element)); | |
| } | |
| bool allowsAttribute(Element element, String attributeName, String value) { | |
| var tagName = Element._safeTagName(element); | |
| if (allowedUriAttributes.contains('$tagName::$attributeName')) { | |
| return uriPolicy!.allowsUri(value); | |
| } else if (allowedUriAttributes.contains('*::$attributeName')) { | |
| return uriPolicy!.allowsUri(value); | |
| } else if (allowedAttributes.contains('$tagName::$attributeName')) { | |
| return true; | |
| } else if (allowedAttributes.contains('*::$attributeName')) { | |
| return true; | |
| } else if (allowedAttributes.contains('$tagName::*')) { | |
| return true; | |
| } else if (allowedAttributes.contains('*::*')) { | |
| return true; | |
| } | |
| return false; | |
| } | |
| } | |
| class _CustomElementNodeValidator extends _SimpleNodeValidator { | |
| final bool allowTypeExtension; | |
| final bool allowCustomTag; | |
| _CustomElementNodeValidator( | |
| UriPolicy uriPolicy, | |
| Iterable<String> allowedElements, | |
| Iterable<String>? allowedAttributes, | |
| Iterable<String>? allowedUriAttributes, | |
| bool allowTypeExtension, | |
| bool allowCustomTag) | |
| : this.allowTypeExtension = allowTypeExtension == true, | |
| this.allowCustomTag = allowCustomTag == true, | |
| super(uriPolicy, | |
| allowedElements: allowedElements, | |
| allowedAttributes: allowedAttributes, | |
| allowedUriAttributes: allowedUriAttributes); | |
| bool allowsElement(Element element) { | |
| if (allowTypeExtension) { | |
| var isAttr = element.attributes['is']; | |
| if (isAttr != null) { | |
| return allowedElements.contains(isAttr.toUpperCase()) && | |
| allowedElements.contains(Element._safeTagName(element)); | |
| } | |
| } | |
| return allowCustomTag && | |
| allowedElements.contains(Element._safeTagName(element)); | |
| } | |
| bool allowsAttribute(Element element, String attributeName, String value) { | |
| if (allowsElement(element)) { | |
| if (allowTypeExtension && | |
| attributeName == 'is' && | |
| allowedElements.contains(value.toUpperCase())) { | |
| return true; | |
| } | |
| return super.allowsAttribute(element, attributeName, value); | |
| } | |
| return false; | |
| } | |
| } | |
| class _TemplatingNodeValidator extends _SimpleNodeValidator { | |
| static const _TEMPLATE_ATTRS = const <String>[ | |
| 'bind', | |
| 'if', | |
| 'ref', | |
| 'repeat', | |
| 'syntax' | |
| ]; | |
| final Set<String> _templateAttrs; | |
| _TemplatingNodeValidator() | |
| : _templateAttrs = new Set<String>.from(_TEMPLATE_ATTRS), | |
| super(null, | |
| allowedElements: ['TEMPLATE'], | |
| allowedAttributes: | |
| _TEMPLATE_ATTRS.map((attr) => 'TEMPLATE::$attr')) {} | |
| bool allowsAttribute(Element element, String attributeName, String value) { | |
| if (super.allowsAttribute(element, attributeName, value)) { | |
| return true; | |
| } | |
| if (attributeName == 'template' && value == "") { | |
| return true; | |
| } | |
| if (element.attributes['template'] == "") { | |
| return _templateAttrs.contains(attributeName); | |
| } | |
| return false; | |
| } | |
| } | |
| class _SvgNodeValidator implements NodeValidator { | |
| bool allowsElement(Element element) { | |
| if (element is svg.ScriptElement) { | |
| return false; | |
| } | |
| // Firefox 37 has issues with creating foreign elements inside a | |
| // foreignobject tag as SvgElement. We don't want foreignobject contents | |
| // anyway, so just remove the whole tree outright. And we can't rely | |
| // on IE recognizing the SvgForeignObject type, so go by tagName. Bug 23144 | |
| if (element is svg.SvgElement && | |
| Element._safeTagName(element) == 'foreignObject') { | |
| return false; | |
| } | |
| if (element is svg.SvgElement) { | |
| return true; | |
| } | |
| return false; | |
| } | |
| bool allowsAttribute(Element element, String attributeName, String value) { | |
| if (attributeName == 'is' || attributeName.startsWith('on')) { | |
| return false; | |
| } | |
| return allowsElement(element); | |
| } | |
| } |