blob: 92cfb3ad16c84acf3eb7e8a07e429aabb2c439eb [file] [log] [blame]
// 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);
}
}