| OLD | NEW |
| 1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file | 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 | 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. | 3 // BSD-style license that can be found in the LICENSE file. |
| 4 | 4 |
| 5 part of dart.dom.html; | 5 part of dart.dom.html; |
| 6 | 6 |
| 7 | |
| 8 /** | 7 /** |
| 9 * Interface used to validate that only accepted elements and attributes are | 8 * Interface used to validate that only accepted elements and attributes are |
| 10 * allowed while parsing HTML strings into DOM nodes. | 9 * allowed while parsing HTML strings into DOM nodes. |
| 11 * | 10 * |
| 12 * In general, customization of validation behavior should be done via the | 11 * In general, customization of validation behavior should be done via the |
| 13 * [NodeValidatorBuilder] class to mitigate the chances of incorrectly | 12 * [NodeValidatorBuilder] class to mitigate the chances of incorrectly |
| 14 * implementing validation rules. | 13 * implementing validation rules. |
| 15 */ | 14 */ |
| 16 abstract class NodeValidator { | 15 abstract class NodeValidator { |
| 17 | |
| 18 /** | 16 /** |
| 19 * Construct a default NodeValidator which only accepts whitelisted HTML5 | 17 * Construct a default NodeValidator which only accepts whitelisted HTML5 |
| 20 * elements and attributes. | 18 * elements and attributes. |
| 21 * | 19 * |
| 22 * If a uriPolicy is not specified then the default uriPolicy will be used. | 20 * If a uriPolicy is not specified then the default uriPolicy will be used. |
| 23 */ | 21 */ |
| 24 factory NodeValidator({UriPolicy uriPolicy}) => | 22 factory NodeValidator({UriPolicy uriPolicy}) => |
| 25 new _Html5NodeValidator(uriPolicy: uriPolicy); | 23 new _Html5NodeValidator(uriPolicy: uriPolicy); |
| 26 | 24 |
| 27 factory NodeValidator.throws(NodeValidator base) => | 25 factory NodeValidator.throws(NodeValidator base) => |
| (...skipping 17 matching lines...) Expand all Loading... |
| 45 /** | 43 /** |
| 46 * Performs sanitization of a node tree after construction to ensure that it | 44 * Performs sanitization of a node tree after construction to ensure that it |
| 47 * does not contain any disallowed elements or attributes. | 45 * does not contain any disallowed elements or attributes. |
| 48 * | 46 * |
| 49 * In general custom implementations of this class should not be necessary and | 47 * In general custom implementations of this class should not be necessary and |
| 50 * all validation customization should be done in custom NodeValidators, but | 48 * all validation customization should be done in custom NodeValidators, but |
| 51 * custom implementations of this class can be created to perform more complex | 49 * custom implementations of this class can be created to perform more complex |
| 52 * tree sanitization. | 50 * tree sanitization. |
| 53 */ | 51 */ |
| 54 abstract class NodeTreeSanitizer { | 52 abstract class NodeTreeSanitizer { |
| 55 | |
| 56 /** | 53 /** |
| 57 * Constructs a default tree sanitizer which will remove all elements and | 54 * Constructs a default tree sanitizer which will remove all elements and |
| 58 * attributes which are not allowed by the provided validator. | 55 * attributes which are not allowed by the provided validator. |
| 59 */ | 56 */ |
| 60 factory NodeTreeSanitizer(NodeValidator validator) => | 57 factory NodeTreeSanitizer(NodeValidator validator) => |
| 61 new _ValidatingTreeSanitizer(validator); | 58 new _ValidatingTreeSanitizer(validator); |
| 62 | 59 |
| 63 /** | 60 /** |
| 64 * Called with the root of the tree which is to be sanitized. | 61 * Called with the root of the tree which is to be sanitized. |
| 65 * | 62 * |
| (...skipping 52 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 118 * (such as https://example.com:80). | 115 * (such as https://example.com:80). |
| 119 */ | 116 */ |
| 120 class _SameOriginUriPolicy implements UriPolicy { | 117 class _SameOriginUriPolicy implements UriPolicy { |
| 121 final AnchorElement _hiddenAnchor = new AnchorElement(); | 118 final AnchorElement _hiddenAnchor = new AnchorElement(); |
| 122 final Location _loc = window.location; | 119 final Location _loc = window.location; |
| 123 | 120 |
| 124 bool allowsUri(String uri) { | 121 bool allowsUri(String uri) { |
| 125 _hiddenAnchor.href = uri; | 122 _hiddenAnchor.href = uri; |
| 126 // IE leaves an empty hostname for same-origin URIs. | 123 // IE leaves an empty hostname for same-origin URIs. |
| 127 return (_hiddenAnchor.hostname == _loc.hostname && | 124 return (_hiddenAnchor.hostname == _loc.hostname && |
| 128 _hiddenAnchor.port == _loc.port && | 125 _hiddenAnchor.port == _loc.port && |
| 129 _hiddenAnchor.protocol == _loc.protocol) || | 126 _hiddenAnchor.protocol == _loc.protocol) || |
| 130 (_hiddenAnchor.hostname == '' && | 127 (_hiddenAnchor.hostname == '' && |
| 131 _hiddenAnchor.port == '' && | 128 _hiddenAnchor.port == '' && |
| 132 (_hiddenAnchor.protocol == ':' || _hiddenAnchor.protocol == '')); | 129 (_hiddenAnchor.protocol == ':' || _hiddenAnchor.protocol == '')); |
| 133 } | 130 } |
| 134 } | 131 } |
| 135 | 132 |
| 136 | |
| 137 class _ThrowsNodeValidator implements NodeValidator { | 133 class _ThrowsNodeValidator implements NodeValidator { |
| 138 final NodeValidator validator; | 134 final NodeValidator validator; |
| 139 | 135 |
| 140 _ThrowsNodeValidator(this.validator) {} | 136 _ThrowsNodeValidator(this.validator) {} |
| 141 | 137 |
| 142 bool allowsElement(Element element) { | 138 bool allowsElement(Element element) { |
| 143 if (!validator.allowsElement(element)) { | 139 if (!validator.allowsElement(element)) { |
| 144 throw new ArgumentError(Element._safeTagName(element)); | 140 throw new ArgumentError(Element._safeTagName(element)); |
| 145 } | 141 } |
| 146 return true; | 142 return true; |
| 147 } | 143 } |
| 148 | 144 |
| 149 bool allowsAttribute(Element element, String attributeName, String value) { | 145 bool allowsAttribute(Element element, String attributeName, String value) { |
| 150 if (!validator.allowsAttribute(element, attributeName, value)) { | 146 if (!validator.allowsAttribute(element, attributeName, value)) { |
| 151 throw new ArgumentError('${Element._safeTagName(element)}[$attributeName="
$value"]'); | 147 throw new ArgumentError( |
| 148 '${Element._safeTagName(element)}[$attributeName="$value"]'); |
| 152 } | 149 } |
| 153 } | 150 } |
| 154 } | 151 } |
| 155 | 152 |
| 156 | |
| 157 /** | 153 /** |
| 158 * Standard tree sanitizer which validates a node tree against the provided | 154 * Standard tree sanitizer which validates a node tree against the provided |
| 159 * validator and removes any nodes or attributes which are not allowed. | 155 * validator and removes any nodes or attributes which are not allowed. |
| 160 */ | 156 */ |
| 161 class _ValidatingTreeSanitizer implements NodeTreeSanitizer { | 157 class _ValidatingTreeSanitizer implements NodeTreeSanitizer { |
| 162 NodeValidator validator; | 158 NodeValidator validator; |
| 163 _ValidatingTreeSanitizer(this.validator) {} | 159 _ValidatingTreeSanitizer(this.validator) {} |
| 164 | 160 |
| 165 void sanitizeTree(Node node) { | 161 void sanitizeTree(Node node) { |
| 166 void walk(Node node, Node parent) { | 162 void walk(Node node, Node parent) { |
| (...skipping 11 matching lines...) Expand all Loading... |
| 178 // children of node and, but we have no way of getting to the next | 174 // children of node and, but we have no way of getting to the next |
| 179 // child, so start again from the last child. | 175 // child, so start again from the last child. |
| 180 _removeNode(child, node); | 176 _removeNode(child, node); |
| 181 child = null; | 177 child = null; |
| 182 nextChild = node.lastChild; | 178 nextChild = node.lastChild; |
| 183 } | 179 } |
| 184 if (child != null) walk(child, node); | 180 if (child != null) walk(child, node); |
| 185 child = nextChild; | 181 child = nextChild; |
| 186 } | 182 } |
| 187 } | 183 } |
| 184 |
| 188 walk(node, null); | 185 walk(node, null); |
| 189 } | 186 } |
| 190 | 187 |
| 191 /// Aggressively try to remove node. | 188 /// Aggressively try to remove node. |
| 192 void _removeNode(Node node, Node parent) { | 189 void _removeNode(Node node, Node parent) { |
| 193 // If we have the parent, it's presumably already passed more sanitization | 190 // If we have the parent, it's presumably already passed more sanitization |
| 194 // or is the fragment, so ask it to remove the child. And if that fails | 191 // or is the fragment, so ask it to remove the child. And if that fails |
| 195 // try to set the outer html. | 192 // try to set the outer html. |
| 196 if (parent == null) { | 193 if (parent == null) { |
| 197 node.remove(); | 194 node.remove(); |
| (...skipping 20 matching lines...) Expand all Loading... |
| 218 var isAttr; | 215 var isAttr; |
| 219 try { | 216 try { |
| 220 // If getting/indexing attributes throws, count that as corrupt. | 217 // If getting/indexing attributes throws, count that as corrupt. |
| 221 attrs = element.attributes; | 218 attrs = element.attributes; |
| 222 isAttr = attrs['is']; | 219 isAttr = attrs['is']; |
| 223 var corruptedTest1 = Element._hasCorruptedAttributes(element); | 220 var corruptedTest1 = Element._hasCorruptedAttributes(element); |
| 224 | 221 |
| 225 // On IE, erratically, the hasCorruptedAttributes test can return false, | 222 // On IE, erratically, the hasCorruptedAttributes test can return false, |
| 226 // even though it clearly is corrupted. A separate copy of the test | 223 // even though it clearly is corrupted. A separate copy of the test |
| 227 // inlining just the basic check seems to help. | 224 // inlining just the basic check seems to help. |
| 228 corrupted = corruptedTest1 ? true : Element._hasCorruptedAttributesAdditio
nalCheck(element); | 225 corrupted = corruptedTest1 |
| 229 } catch(e) {} | 226 ? true |
| 227 : Element._hasCorruptedAttributesAdditionalCheck(element); |
| 228 } catch (e) {} |
| 230 var elementText = 'element unprintable'; | 229 var elementText = 'element unprintable'; |
| 231 try { | 230 try { |
| 232 elementText = element.toString(); | 231 elementText = element.toString(); |
| 233 } catch(e) {} | 232 } catch (e) {} |
| 234 try { | 233 try { |
| 235 var elementTagName = Element._safeTagName(element); | 234 var elementTagName = Element._safeTagName(element); |
| 236 _sanitizeElement(element, parent, corrupted, elementText, elementTagName, | 235 _sanitizeElement(element, parent, corrupted, elementText, elementTagName, |
| 237 attrs, isAttr); | 236 attrs, isAttr); |
| 238 } on ArgumentError { // Thrown by _ThrowsNodeValidator | 237 } on ArgumentError { |
| 238 // Thrown by _ThrowsNodeValidator |
| 239 rethrow; | 239 rethrow; |
| 240 } catch(e) { // Unexpected exception sanitizing -> remove | 240 } catch (e) { |
| 241 // Unexpected exception sanitizing -> remove |
| 241 _removeNode(element, parent); | 242 _removeNode(element, parent); |
| 242 window.console.warn('Removing corrupted element $elementText'); | 243 window.console.warn('Removing corrupted element $elementText'); |
| 243 } | 244 } |
| 244 } | 245 } |
| 245 | 246 |
| 246 /// Having done basic sanity checking on the element, and computed the | 247 /// Having done basic sanity checking on the element, and computed the |
| 247 /// important attributes we want to check, remove it if it's not valid | 248 /// important attributes we want to check, remove it if it's not valid |
| 248 /// or not allowed, either as a whole or particular attributes. | 249 /// or not allowed, either as a whole or particular attributes. |
| 249 void _sanitizeElement(Element element, Node parent, bool corrupted, | 250 void _sanitizeElement(Element element, Node parent, bool corrupted, |
| 250 String text, String tag, Map attrs, String isAttr) { | 251 String text, String tag, Map attrs, String isAttr) { |
| 251 if (false != corrupted) { | 252 if (false != corrupted) { |
| 252 _removeNode(element, parent); | 253 _removeNode(element, parent); |
| 253 window.console.warn( | 254 window.console |
| 254 'Removing element due to corrupted attributes on <$text>'); | 255 .warn('Removing element due to corrupted attributes on <$text>'); |
| 255 return; | 256 return; |
| 256 } | 257 } |
| 257 if (!validator.allowsElement(element)) { | 258 if (!validator.allowsElement(element)) { |
| 258 _removeNode(element, parent); | 259 _removeNode(element, parent); |
| 259 window.console.warn( | 260 window.console.warn('Removing disallowed element <$tag> from $parent'); |
| 260 'Removing disallowed element <$tag> from $parent'); | |
| 261 return; | 261 return; |
| 262 } | 262 } |
| 263 | 263 |
| 264 if (isAttr != null) { | 264 if (isAttr != null) { |
| 265 if (!validator.allowsAttribute(element, 'is', isAttr)) { | 265 if (!validator.allowsAttribute(element, 'is', isAttr)) { |
| 266 _removeNode(element, parent); | 266 _removeNode(element, parent); |
| 267 window.console.warn('Removing disallowed type extension ' | 267 window.console.warn('Removing disallowed type extension ' |
| 268 '<$tag is="$isAttr">'); | 268 '<$tag is="$isAttr">'); |
| 269 return; | 269 return; |
| 270 } | 270 } |
| 271 } | 271 } |
| 272 | 272 |
| 273 // TODO(blois): Need to be able to get all attributes, irrespective of | 273 // TODO(blois): Need to be able to get all attributes, irrespective of |
| 274 // XMLNS. | 274 // XMLNS. |
| 275 var keys = attrs.keys.toList(); | 275 var keys = attrs.keys.toList(); |
| 276 for (var i = attrs.length - 1; i >= 0; --i) { | 276 for (var i = attrs.length - 1; i >= 0; --i) { |
| 277 var name = keys[i]; | 277 var name = keys[i]; |
| 278 if (!validator.allowsAttribute(element, name.toLowerCase(), | 278 if (!validator.allowsAttribute( |
| 279 attrs[name])) { | 279 element, name.toLowerCase(), attrs[name])) { |
| 280 window.console.warn('Removing disallowed attribute ' | 280 window.console.warn('Removing disallowed attribute ' |
| 281 '<$tag $name="${attrs[name]}">'); | 281 '<$tag $name="${attrs[name]}">'); |
| 282 attrs.remove(name); | 282 attrs.remove(name); |
| 283 } | 283 } |
| 284 } | 284 } |
| 285 | 285 |
| 286 if (element is TemplateElement) { | 286 if (element is TemplateElement) { |
| 287 TemplateElement template = element; | 287 TemplateElement template = element; |
| 288 sanitizeTree(template.content); | 288 sanitizeTree(template.content); |
| 289 } | 289 } |
| 290 } | 290 } |
| 291 | 291 |
| 292 /// Sanitize the node and its children recursively. | 292 /// Sanitize the node and its children recursively. |
| 293 void sanitizeNode(Node node, Node parent) { | 293 void sanitizeNode(Node node, Node parent) { |
| 294 switch (node.nodeType) { | 294 switch (node.nodeType) { |
| 295 case Node.ELEMENT_NODE: | 295 case Node.ELEMENT_NODE: |
| 296 _sanitizeUntrustedElement(node, parent); | 296 _sanitizeUntrustedElement(node, parent); |
| 297 break; | 297 break; |
| 298 case Node.COMMENT_NODE: | 298 case Node.COMMENT_NODE: |
| 299 case Node.DOCUMENT_FRAGMENT_NODE: | 299 case Node.DOCUMENT_FRAGMENT_NODE: |
| 300 case Node.TEXT_NODE: | 300 case Node.TEXT_NODE: |
| 301 case Node.CDATA_SECTION_NODE: | 301 case Node.CDATA_SECTION_NODE: |
| 302 break; | 302 break; |
| 303 default: | 303 default: |
| 304 _removeNode(node, parent); | 304 _removeNode(node, parent); |
| 305 } | 305 } |
| 306 } | 306 } |
| 307 } | 307 } |
| OLD | NEW |