Index: tools/dom/src/NodeValidatorBuilder.dart |
diff --git a/tools/dom/src/NodeValidatorBuilder.dart b/tools/dom/src/NodeValidatorBuilder.dart |
new file mode 100644 |
index 0000000000000000000000000000000000000000..c5f0c80542a90835393044215c1d36965ea7bdbf |
--- /dev/null |
+++ b/tools/dom/src/NodeValidatorBuilder.dart |
@@ -0,0 +1,453 @@ |
+// 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 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; |
+ if (attributes != null) { |
+ attrs = |
+ attributes.map((name) => '$tagNameUpper::${name.toLowerCase()}'); |
+ } |
+ var uriAttrs; |
+ if (uriAttributes != null) { |
+ uriAttrs = |
+ uriAttributes.map((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; |
+ if (attributes != null) { |
+ attrs = |
+ attributes.map((name) => '$baseNameUpper::${name.toLowerCase()}'); |
+ } |
+ var uriAttrs; |
+ if (uriAttributes != null) { |
+ uriAttrs = |
+ uriAttributes.map((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; |
+ final Set<String> allowedAttributes; |
+ final Set<String> allowedUriAttributes; |
+ final UriPolicy uriPolicy; |
+ |
+ factory _SimpleNodeValidator.allowNavigation(UriPolicy uriPolicy) { |
+ return new _SimpleNodeValidator(uriPolicy, |
+ allowedElements: [ |
+ 'A', |
+ 'FORM'], |
+ allowedAttributes: [ |
+ '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: [ |
+ 'A::href', |
+ 'FORM::action', |
+ ]); |
+ } |
+ |
+ factory _SimpleNodeValidator.allowImages(UriPolicy uriPolicy) { |
+ return new _SimpleNodeValidator(uriPolicy, |
+ allowedElements: [ |
+ 'IMG' |
+ ], |
+ allowedAttributes: [ |
+ 'IMG::align', |
+ 'IMG::alt', |
+ 'IMG::border', |
+ 'IMG::height', |
+ 'IMG::hspace', |
+ 'IMG::ismap', |
+ 'IMG::name', |
+ 'IMG::usemap', |
+ 'IMG::vspace', |
+ 'IMG::width', |
+ ], |
+ allowedUriAttributes: [ |
+ 'IMG::src', |
+ ]); |
+ } |
+ |
+ factory _SimpleNodeValidator.allowTextElements() { |
+ return new _SimpleNodeValidator(null, |
+ allowedElements: [ |
+ '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 = allowedElements != null ? |
+ new Set.from(allowedElements) : new Set(), |
+ this.allowedAttributes = allowedAttributes != null ? |
+ new Set.from(allowedAttributes) : new Set(), |
+ this.allowedUriAttributes = allowedUriAttributes != null ? |
+ new Set.from(allowedUriAttributes) : new Set(); |
+ |
+ bool allowsElement(Element element) { |
+ return allowedElements.contains(element.tagName); |
+ } |
+ |
+ bool allowsAttribute(Element element, String attributeName, String value) { |
+ var tagName = element.tagName; |
+ 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): |
+ |
+ super(uriPolicy, |
+ allowedElements: allowedElements, |
+ allowedAttributes: allowedAttributes, |
+ allowedUriAttributes: allowedUriAttributes), |
+ this.allowTypeExtension = allowTypeExtension == true, |
+ this.allowCustomTag = allowCustomTag == true; |
+ |
+ bool allowsElement(Element element) { |
+ if (allowTypeExtension) { |
+ var isAttr = element.attributes['is']; |
+ if (isAttr != null) { |
+ return allowedElements.contains(isAttr.toUpperCase()) && |
+ allowedElements.contains(element.tagName); |
+ } |
+ } |
+ return allowCustomTag && allowedElements.contains(element.tagName); |
+ } |
+ |
+ 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(): |
+ super(null, |
+ allowedElements: [ |
+ 'TEMPLATE' |
+ ], |
+ allowedAttributes: _TEMPLATE_ATTRS.map((attr) => 'TEMPLATE::$attr')), |
+ _templateAttrs = new Set<String>.from(_TEMPLATE_ATTRS) { |
+ } |
+ |
+ 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; |
+ } |
+ 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); |
+ } |
+} |