OLD | NEW |
(Empty) | |
| 1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file |
| 2 // for details. All rights reserved. Use of this source code is governed by a |
| 3 // BSD-style license that can be found in the LICENSE file. |
| 4 |
| 5 part of dart.dom.html; |
| 6 |
| 7 |
| 8 /** |
| 9 * Class which helps construct standard node validation policies. |
| 10 * |
| 11 * By default this will not accept anything, but the 'allow*' functions can be |
| 12 * used to expand what types of elements or attributes are allowed. |
| 13 * |
| 14 * All allow functions are additive- elements will be accepted if they are |
| 15 * accepted by any specific rule. |
| 16 * |
| 17 * It is important to remember that sanitization is not just intended to prevent |
| 18 * cross-site scripting attacks, but also to prevent information from being |
| 19 * displayed in unexpected ways. For example something displaying basic |
| 20 * formatted text may not expect `<video>` tags to appear. In this case an |
| 21 * empty NodeValidatorBuilder with just [allowTextElements] might be |
| 22 * appropriate. |
| 23 */ |
| 24 class NodeValidatorBuilder implements NodeValidator { |
| 25 |
| 26 final List<NodeValidator> _validators = <NodeValidator>[]; |
| 27 |
| 28 NodeValidatorBuilder() { |
| 29 } |
| 30 |
| 31 /** |
| 32 * Creates a new NodeValidatorBuilder which accepts common constructs. |
| 33 * |
| 34 * By default this will accept HTML5 elements and attributes with the default |
| 35 * [UriPolicy] and templating elements. |
| 36 * |
| 37 * Notable syntax which is filtered: |
| 38 * |
| 39 * * Only known-good HTML5 elements and attributes are allowed. |
| 40 * * All URLs must be same-origin, use [allowNavigation] and [allowImages] to |
| 41 * specify additional URI policies. |
| 42 * * Inline-styles are not allowed. |
| 43 * * Custom element tags are disallowed, use [allowCustomElement]. |
| 44 * * Custom tags extensions are disallowed, use [allowTagExtension]. |
| 45 * * SVG Elements are not allowed, use [allowSvg]. |
| 46 * |
| 47 * For scenarios where the HTML should only contain formatted text |
| 48 * [allowTextElements] is more appropriate. |
| 49 * |
| 50 * Use [allowSvg] to allow SVG elements. |
| 51 */ |
| 52 NodeValidatorBuilder.common() { |
| 53 allowHtml5(); |
| 54 allowTemplating(); |
| 55 } |
| 56 |
| 57 /** |
| 58 * Allows navigation elements- Form and Anchor tags, along with common |
| 59 * attributes. |
| 60 * |
| 61 * The UriPolicy can be used to restrict the locations the navigation elements |
| 62 * are allowed to direct to. By default this will use the default [UriPolicy]. |
| 63 */ |
| 64 void allowNavigation([UriPolicy uriPolicy]) { |
| 65 if (uriPolicy == null) { |
| 66 uriPolicy = new UriPolicy(); |
| 67 } |
| 68 add(new _SimpleNodeValidator.allowNavigation(uriPolicy)); |
| 69 } |
| 70 |
| 71 /** |
| 72 * Allows image elements. |
| 73 * |
| 74 * The UriPolicy can be used to restrict the locations the images may be |
| 75 * loaded from. By default this will use the default [UriPolicy]. |
| 76 */ |
| 77 void allowImages([UriPolicy uriPolicy]) { |
| 78 if (uriPolicy == null) { |
| 79 uriPolicy = new UriPolicy(); |
| 80 } |
| 81 add(new _SimpleNodeValidator.allowImages(uriPolicy)); |
| 82 } |
| 83 |
| 84 /** |
| 85 * Allow basic text elements. |
| 86 * |
| 87 * This allows a subset of HTML5 elements, specifically just these tags and |
| 88 * no attributes. |
| 89 * |
| 90 * * B |
| 91 * * BLOCKQUOTE |
| 92 * * BR |
| 93 * * EM |
| 94 * * H1 |
| 95 * * H2 |
| 96 * * H3 |
| 97 * * H4 |
| 98 * * H5 |
| 99 * * H6 |
| 100 * * HR |
| 101 * * I |
| 102 * * LI |
| 103 * * OL |
| 104 * * P |
| 105 * * SPAN |
| 106 * * UL |
| 107 */ |
| 108 void allowTextElements() { |
| 109 add(new _SimpleNodeValidator.allowTextElements()); |
| 110 } |
| 111 |
| 112 /** |
| 113 * Allow common safe HTML5 elements and attributes. |
| 114 * |
| 115 * This list is based off of the Caja whitelists at: |
| 116 * https://code.google.com/p/google-caja/wiki/CajaWhitelists. |
| 117 * |
| 118 * Common things which are not allowed are script elements, style attributes |
| 119 * and any script handlers. |
| 120 */ |
| 121 void allowHtml5({UriPolicy uriPolicy}) { |
| 122 add(new _Html5NodeValidator(uriPolicy: uriPolicy)); |
| 123 } |
| 124 |
| 125 /** |
| 126 * Allow SVG elements and attributes except for known bad ones. |
| 127 */ |
| 128 void allowSvg() { |
| 129 add(new _SvgNodeValidator()); |
| 130 } |
| 131 |
| 132 /** |
| 133 * Allow custom elements with the specified tag name and specified attributes. |
| 134 * |
| 135 * This will allow the elements as custom tags (such as <x-foo></x-foo>), |
| 136 * but will not allow tag extensions. Use [allowTagExtension] to allow |
| 137 * tag extensions. |
| 138 */ |
| 139 void allowCustomElement(String tagName, |
| 140 {UriPolicy uriPolicy, |
| 141 Iterable<String> attributes, |
| 142 Iterable<String> uriAttributes}) { |
| 143 |
| 144 var tagNameUpper = tagName.toUpperCase(); |
| 145 var attrs; |
| 146 if (attributes != null) { |
| 147 attrs = |
| 148 attributes.map((name) => '$tagNameUpper::${name.toLowerCase()}'); |
| 149 } |
| 150 var uriAttrs; |
| 151 if (uriAttributes != null) { |
| 152 uriAttrs = |
| 153 uriAttributes.map((name) => '$tagNameUpper::${name.toLowerCase()}'); |
| 154 } |
| 155 if (uriPolicy == null) { |
| 156 uriPolicy = new UriPolicy(); |
| 157 } |
| 158 |
| 159 add(new _CustomElementNodeValidator( |
| 160 uriPolicy, |
| 161 [tagNameUpper], |
| 162 attrs, |
| 163 uriAttrs, |
| 164 false, |
| 165 true)); |
| 166 } |
| 167 |
| 168 /** |
| 169 * Allow custom tag extensions with the specified type name and specified |
| 170 * attributes. |
| 171 * |
| 172 * This will allow tag extensions (such as <div is="x-foo"></div>), |
| 173 * but will not allow custom tags. Use [allowCustomElement] to allow |
| 174 * custom tags. |
| 175 */ |
| 176 void allowTagExtension(String tagName, String baseName, |
| 177 {UriPolicy uriPolicy, |
| 178 Iterable<String> attributes, |
| 179 Iterable<String> uriAttributes}) { |
| 180 |
| 181 var baseNameUpper = baseName.toUpperCase(); |
| 182 var tagNameUpper = tagName.toUpperCase(); |
| 183 var attrs; |
| 184 if (attributes != null) { |
| 185 attrs = |
| 186 attributes.map((name) => '$baseNameUpper::${name.toLowerCase()}'); |
| 187 } |
| 188 var uriAttrs; |
| 189 if (uriAttributes != null) { |
| 190 uriAttrs = |
| 191 uriAttributes.map((name) => '$baseNameUpper::${name.toLowerCase()}'); |
| 192 } |
| 193 if (uriPolicy == null) { |
| 194 uriPolicy = new UriPolicy(); |
| 195 } |
| 196 |
| 197 add(new _CustomElementNodeValidator( |
| 198 uriPolicy, |
| 199 [tagNameUpper, baseNameUpper], |
| 200 attrs, |
| 201 uriAttrs, |
| 202 true, |
| 203 false)); |
| 204 } |
| 205 |
| 206 void allowElement(String tagName, {UriPolicy uriPolicy, |
| 207 Iterable<String> attributes, |
| 208 Iterable<String> uriAttributes}) { |
| 209 |
| 210 allowCustomElement(tagName, uriPolicy: uriPolicy, |
| 211 attributes: attributes, |
| 212 uriAttributes: uriAttributes); |
| 213 } |
| 214 |
| 215 /** |
| 216 * Allow templating elements (such as <template> and template-related |
| 217 * attributes. |
| 218 * |
| 219 * This still requires other validators to allow regular attributes to be |
| 220 * bound (such as [allowHtml5]). |
| 221 */ |
| 222 void allowTemplating() { |
| 223 add(new _TemplatingNodeValidator()); |
| 224 } |
| 225 |
| 226 /** |
| 227 * Add an additional validator to the current list of validators. |
| 228 * |
| 229 * Elements and attributes will be accepted if they are accepted by any |
| 230 * validators. |
| 231 */ |
| 232 void add(NodeValidator validator) { |
| 233 _validators.add(validator); |
| 234 } |
| 235 |
| 236 bool allowsElement(Element element) { |
| 237 return _validators.any((v) => v.allowsElement(element)); |
| 238 } |
| 239 |
| 240 bool allowsAttribute(Element element, String attributeName, String value) { |
| 241 return _validators.any( |
| 242 (v) => v.allowsAttribute(element, attributeName, value)); |
| 243 } |
| 244 } |
| 245 |
| 246 class _SimpleNodeValidator implements NodeValidator { |
| 247 final Set<String> allowedElements; |
| 248 final Set<String> allowedAttributes; |
| 249 final Set<String> allowedUriAttributes; |
| 250 final UriPolicy uriPolicy; |
| 251 |
| 252 factory _SimpleNodeValidator.allowNavigation(UriPolicy uriPolicy) { |
| 253 return new _SimpleNodeValidator(uriPolicy, |
| 254 allowedElements: [ |
| 255 'A', |
| 256 'FORM'], |
| 257 allowedAttributes: [ |
| 258 'A::accesskey', |
| 259 'A::coords', |
| 260 'A::hreflang', |
| 261 'A::name', |
| 262 'A::shape', |
| 263 'A::tabindex', |
| 264 'A::target', |
| 265 'A::type', |
| 266 'FORM::accept', |
| 267 'FORM::autocomplete', |
| 268 'FORM::enctype', |
| 269 'FORM::method', |
| 270 'FORM::name', |
| 271 'FORM::novalidate', |
| 272 'FORM::target', |
| 273 ], |
| 274 allowedUriAttributes: [ |
| 275 'A::href', |
| 276 'FORM::action', |
| 277 ]); |
| 278 } |
| 279 |
| 280 factory _SimpleNodeValidator.allowImages(UriPolicy uriPolicy) { |
| 281 return new _SimpleNodeValidator(uriPolicy, |
| 282 allowedElements: [ |
| 283 'IMG' |
| 284 ], |
| 285 allowedAttributes: [ |
| 286 'IMG::align', |
| 287 'IMG::alt', |
| 288 'IMG::border', |
| 289 'IMG::height', |
| 290 'IMG::hspace', |
| 291 'IMG::ismap', |
| 292 'IMG::name', |
| 293 'IMG::usemap', |
| 294 'IMG::vspace', |
| 295 'IMG::width', |
| 296 ], |
| 297 allowedUriAttributes: [ |
| 298 'IMG::src', |
| 299 ]); |
| 300 } |
| 301 |
| 302 factory _SimpleNodeValidator.allowTextElements() { |
| 303 return new _SimpleNodeValidator(null, |
| 304 allowedElements: [ |
| 305 'B', |
| 306 'BLOCKQUOTE', |
| 307 'BR', |
| 308 'EM', |
| 309 'H1', |
| 310 'H2', |
| 311 'H3', |
| 312 'H4', |
| 313 'H5', |
| 314 'H6', |
| 315 'HR', |
| 316 'I', |
| 317 'LI', |
| 318 'OL', |
| 319 'P', |
| 320 'SPAN', |
| 321 'UL', |
| 322 ]); |
| 323 } |
| 324 |
| 325 /** |
| 326 * Elements must be uppercased tag names. For example `'IMG'`. |
| 327 * Attributes must be uppercased tag name followed by :: followed by |
| 328 * lowercase attribute name. For example `'IMG:src'`. |
| 329 */ |
| 330 _SimpleNodeValidator(this.uriPolicy, |
| 331 {Iterable<String> allowedElements, Iterable<String> allowedAttributes, |
| 332 Iterable<String> allowedUriAttributes}): |
| 333 this.allowedElements = allowedElements != null ? |
| 334 new Set.from(allowedElements) : new Set(), |
| 335 this.allowedAttributes = allowedAttributes != null ? |
| 336 new Set.from(allowedAttributes) : new Set(), |
| 337 this.allowedUriAttributes = allowedUriAttributes != null ? |
| 338 new Set.from(allowedUriAttributes) : new Set(); |
| 339 |
| 340 bool allowsElement(Element element) { |
| 341 return allowedElements.contains(element.tagName); |
| 342 } |
| 343 |
| 344 bool allowsAttribute(Element element, String attributeName, String value) { |
| 345 var tagName = element.tagName; |
| 346 if (allowedUriAttributes.contains('$tagName::$attributeName')) { |
| 347 return uriPolicy.allowsUri(value); |
| 348 } else if (allowedUriAttributes.contains('*::$attributeName')) { |
| 349 return uriPolicy.allowsUri(value); |
| 350 } else if (allowedAttributes.contains('$tagName::$attributeName')) { |
| 351 return true; |
| 352 } else if (allowedAttributes.contains('*::$attributeName')) { |
| 353 return true; |
| 354 } else if (allowedAttributes.contains('$tagName::*')) { |
| 355 return true; |
| 356 } else if (allowedAttributes.contains('*::*')) { |
| 357 return true; |
| 358 } |
| 359 return false; |
| 360 } |
| 361 } |
| 362 |
| 363 class _CustomElementNodeValidator extends _SimpleNodeValidator { |
| 364 final bool allowTypeExtension; |
| 365 final bool allowCustomTag; |
| 366 |
| 367 _CustomElementNodeValidator(UriPolicy uriPolicy, |
| 368 Iterable<String> allowedElements, |
| 369 Iterable<String> allowedAttributes, |
| 370 Iterable<String> allowedUriAttributes, |
| 371 bool allowTypeExtension, |
| 372 bool allowCustomTag): |
| 373 |
| 374 super(uriPolicy, |
| 375 allowedElements: allowedElements, |
| 376 allowedAttributes: allowedAttributes, |
| 377 allowedUriAttributes: allowedUriAttributes), |
| 378 this.allowTypeExtension = allowTypeExtension == true, |
| 379 this.allowCustomTag = allowCustomTag == true; |
| 380 |
| 381 bool allowsElement(Element element) { |
| 382 if (allowTypeExtension) { |
| 383 var isAttr = element.attributes['is']; |
| 384 if (isAttr != null) { |
| 385 return allowedElements.contains(isAttr.toUpperCase()) && |
| 386 allowedElements.contains(element.tagName); |
| 387 } |
| 388 } |
| 389 return allowCustomTag && allowedElements.contains(element.tagName); |
| 390 } |
| 391 |
| 392 bool allowsAttribute(Element element, String attributeName, String value) { |
| 393 if (allowsElement(element)) { |
| 394 if (allowTypeExtension && attributeName == 'is' && |
| 395 allowedElements.contains(value.toUpperCase())) { |
| 396 return true; |
| 397 } |
| 398 return super.allowsAttribute(element, attributeName, value); |
| 399 } |
| 400 return false; |
| 401 } |
| 402 } |
| 403 |
| 404 class _TemplatingNodeValidator extends _SimpleNodeValidator { |
| 405 static const _TEMPLATE_ATTRS = |
| 406 const <String>['bind', 'if', 'ref', 'repeat', 'syntax']; |
| 407 |
| 408 final Set<String> _templateAttrs; |
| 409 |
| 410 _TemplatingNodeValidator(): |
| 411 super(null, |
| 412 allowedElements: [ |
| 413 'TEMPLATE' |
| 414 ], |
| 415 allowedAttributes: _TEMPLATE_ATTRS.map((attr) => 'TEMPLATE::$attr')), |
| 416 _templateAttrs = new Set<String>.from(_TEMPLATE_ATTRS) { |
| 417 } |
| 418 |
| 419 bool allowsAttribute(Element element, String attributeName, String value) { |
| 420 if (super.allowsAttribute(element, attributeName, value)) { |
| 421 return true; |
| 422 } |
| 423 |
| 424 if (attributeName == 'template' && value == "") { |
| 425 return true; |
| 426 } |
| 427 |
| 428 if (element.attributes['template'] == "" ) { |
| 429 return _templateAttrs.contains(attributeName); |
| 430 } |
| 431 return false; |
| 432 } |
| 433 } |
| 434 |
| 435 |
| 436 class _SvgNodeValidator implements NodeValidator { |
| 437 bool allowsElement(Element element) { |
| 438 if (element is svg.ScriptElement) { |
| 439 return false; |
| 440 } |
| 441 if (element is svg.SvgElement) { |
| 442 return true; |
| 443 } |
| 444 return false; |
| 445 } |
| 446 |
| 447 bool allowsAttribute(Element element, String attributeName, String value) { |
| 448 if (attributeName == 'is' || attributeName.startsWith('on')) { |
| 449 return false; |
| 450 } |
| 451 return allowsElement(element); |
| 452 } |
| 453 } |
OLD | NEW |