-
Notifications
You must be signed in to change notification settings - Fork 27.4k
fix($sanitize): prevent clobbered elements from freezing the browser #15699
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
@ngdoc error | ||
@name $sanitize:elclob | ||
@fullName Failed to sanitize html because the element is clobbered | ||
@description | ||
|
||
This error occurs when `$sanitize` sanitizer is unable to traverse the HTML because one or more of the elements in the | ||
HTML have been "clobbered". This could be a sign that the payload contains code attempting to cause a DoS attack on the | ||
browser. | ||
|
||
Typically clobbering breaks the `nextSibling` property on an element so that it points to one of its child nodes. This | ||
makes it impossible to walk the HTML tree without getting stuck in an infinite loop, which causes the browser to freeze. |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -18,6 +18,7 @@ var forEach; | |
var isDefined; | ||
var lowercase; | ||
var noop; | ||
var nodeContains; | ||
var htmlParser; | ||
var htmlSanitizeWriter; | ||
|
||
|
@@ -218,6 +219,11 @@ function $SanitizeProvider() { | |
htmlParser = htmlParserImpl; | ||
htmlSanitizeWriter = htmlSanitizeWriterImpl; | ||
|
||
nodeContains = window.Node.prototype.contains || /** @this */ function(arg) { | ||
// eslint-disable-next-line no-bitwise | ||
return !!(this.compareDocumentPosition(arg) & 16); | ||
}; | ||
|
||
// Regular Expressions for parsing tags and attributes | ||
var SURROGATE_PAIR_REGEXP = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g, | ||
// Match everything outside of normal chars and " (quote character) | ||
|
@@ -381,12 +387,12 @@ function $SanitizeProvider() { | |
if (node.nodeType === 1) { | ||
handler.end(node.nodeName.toLowerCase()); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nodeName can be clobbered. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See #15699 (comment). |
||
} | ||
nextNode = node.nextSibling; | ||
nextNode = getNonDescendant('nextSibling', node); | ||
if (!nextNode) { | ||
while (nextNode == null) { | ||
node = node.parentNode; | ||
node = getNonDescendant('parentNode', node); | ||
if (node === inertBodyElement) break; | ||
nextNode = node.nextSibling; | ||
nextNode = getNonDescendant('nextSibling', node); | ||
if (node.nodeType === 1) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nodeType can be clobberred. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What we are trying to avoid (afaict) is having an infinite loop, while traversing the DOM tree, which will eventually crash the browser. I couldn't find a way to achive this via There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If an infinite loop is the only concern, disregard my comments. JS-based sanitizers can usually fail by making them throw which breaks applications not catching an exception. Commonly, DOM clobbering is used to do that. If exceptions in ngSanitize do not result in apps breaking, it's fine. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I meant that is what we are address in this PR 😃 But if someone uses it differently, it is possible that it breaks the app (as any thrown exception could). Do you think There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If the errors are triggered by processing untrusted HTML (which is likely the most common code), I think the safe default would be to return an empty string instead of throwing and requiring application to catch. But tat this point it's not really a security issue, but should be similar to how other core services behave (i.e. is it expected for them to throw and it should be handled by apps). |
||
handler.end(node.nodeName.toLowerCase()); | ||
} | ||
|
@@ -518,8 +524,17 @@ function $SanitizeProvider() { | |
stripCustomNsAttrs(nextNode); | ||
} | ||
|
||
node = node.nextSibling; | ||
node = getNonDescendant('nextSibling', node); | ||
} | ||
} | ||
|
||
function getNonDescendant(propName, node) { | ||
// An element is clobbered if its `propName` property points to one of its descendants | ||
var nextNode = node[propName]; | ||
if (nextNode && nodeContains.call(node, nextNode)) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. are we bothered about generic descendents or only inputs that are members of a form?
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't know of another usecase that would be affected other than There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hm...it seems that this won't work for <form name="foo>
<input name="bar" />
<img name="baz" />
</form> document.foo.bar //--> `<input ... />`
document.foo.bar.form //--> `<form ... >`
document.foo.baz //--> `<img ... />`
document.foo.baz.form //--> undefined There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OK, let's leave it as it is then |
||
throw $sanitizeMinErr('elclob', 'Failed to sanitize html because the element is clobbered: {0}', node.outerHTML || node.outerText); | ||
} | ||
return nextNode; | ||
} | ||
} | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
compareDocumentPosition
can also be clobbered. That will throw an exception - is that OK for your case i.e. is the exception caught and safely resolved?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is OK afaict. What we are trying to avoid is having an infinite loop, while traversing the DOM tree, which will eventually crash the browser.