Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Registering hook leads to infinite loop in IE 10 / IE 11 #89

Closed
luhmann opened this issue Sep 14, 2015 · 12 comments
Closed

Registering hook leads to infinite loop in IE 10 / IE 11 #89

luhmann opened this issue Sep 14, 2015 · 12 comments

Comments

@luhmann
Copy link

luhmann commented Sep 14, 2015

Either I am doing something very obvious wrong or registering a hook (I tested afterSanitizeElements and beforeSanitizeElements) and then calling the sanitize-method leads to an infinite loop in IE 10 and IE 11, which hangs up the browser. If I remove the hook, everything works fine. My JS Code just does this:

DOMPurify.addHook('afterSanitizeElements', function(node) {
  if (node.nodeType && node.nodeType === document.TEXT_NODE) {
    node.textContent = 'foo';
  }

  return node;
});
var editor = document.querySelector('.editor');
document.querySelector('.button').addEventListener('click', function () {
  var dirty = '<div><p>This is a beatufiul text</p><p>This is too</p></div>';
  editor.innerHTML = DOMPurify.sanitize(dirty);
});

I would expect it to keep the DOM-structure intact and just replace the text-nodes with "foo".

I created a pen which is enough to cause the error in IE 10 and IE 11. Other browsers (I tested Edge, Chrome, Firefox and Safari) look fine to me: http://codepen.io/anon/pen/YywGXz

@cure53
Copy link
Owner

cure53 commented Sep 14, 2015

Oh, that sounds bad, I'll have a look right away!

@cure53
Copy link
Owner

cure53 commented Sep 14, 2015

I can reproduce the issue.

I think the problem here is the following: MSIE seems to recurse over this operation, because it appears to create a new textNode each time the text of the existing textNode is set.

That means, that upon setting the text, a new node appears that needs to be sanitized, then text is being set, creating a new textNode... and so on. Edge by the way has the same problem. I am looking into how to fix this now...

This example hangs:

DOMPurify.addHook('afterSanitizeElements', function(node) {
  if (node.nodeType && node.nodeType === document.TEXT_NODE) {
        node.textContent = 'foo';
  }
  return node;
});

This example does not:

DOMPurify.addHook('afterSanitizeElements', function(node) {
  if (node.nodeType && node.nodeType === document.TEXT_NODE) {
        node.innerText = 'foo';
  }
  return node;
});

cure53 added a commit that referenced this issue Sep 14, 2015
Review needed!
@cure53
Copy link
Owner

cure53 commented Sep 14, 2015

Okay, I came up with a first possible fix:

https://github.com/cure53/DOMPurify/compare/Experimental_Fix_for_89?expand=1

This fix stores a copy of the currently processed mode as oldNode, then compares the new incoming node (then currentNode) to the old node. If the new node and the old node are identical, the node iterator is told to step ahead one call and continue from there. This fixes the problem of IE and doesn't seem to affect other browsers. The tests are still running as expected.

This could be a security critical change so I'd kindly ask for review, @fhemberger and @neilj. I also think the fix is ugly but I cannot come up with something better right now.

@neilj
Copy link
Contributor

neilj commented Sep 14, 2015

A few thoughts:

  1. I'm not convinced that the IE behaviour is wrong. The spec is slightly ambiguous, but I could certainly interpret it as saying you should replace the whole text node. The rules of the NodeIterator means the new node then comes after the cursor, so is returned next. As such, I don't see anything wrong that necessarily needs to change with the current code in DOMPurify: this should be fixed by the code using it (my recommendation is to simply replace textContent with data, since this then sets the data of the text node itself, which is what was originally intended). You could just as easily create an infinite loop by inserting a new element after the current element in a hook; this doesn't meant the library itself is broken.
  2. If you still do want to patch this particular case in the library, I don't think your fix will work (although I haven't actually checked, as I don't have IE handy). From the description above, the problem is that the text node is replaced. Therefore currentNode === oldNode would always be false, because they are different nodes, so you would still infinite loop. Also, the code formatting is completely inconsistent with the rest of the library:
if(currentNode === oldNode){
            nodeIterator.nextNode()
}

There should be a space after the if and before the {, and the next line is indented too much.

@cure53
Copy link
Owner

cure53 commented Sep 14, 2015

@neilj The hang also happens with data and other properties/methods on Text. It seems only innerHTML and innerText are fine and I would not recommend using them here. IE appears to be "empirically" wrong as all other user agents don't cause a recursion and honestly, the code doesn't look like it should. Yet it still does because we use the node iterator internally. So it is at least partly our fault.

The fix surprisingly works, because in this situation, the result is indeed true. I didn't believe it myself at first but it is the case. So factually, the fix does tackle the problem, just not in a nice or logical way. But that's IE.

About the coding inconsistencies: True, you are 100% right. But at this stage of the fix (merely experimenting in an experimental branch) I don't care too much. Cosmetics when cosmetics are due. Now I want to find a working and reasonable fix.

@neilj
Copy link
Contributor

neilj commented Sep 14, 2015

Fine. So actually IE is doing something very weird, not simply replacing the text node then. In that case, I have one small change I think. Instead of:

if (currentNode === oldNode) {
    nodeIterator.nextNode()
}

I think you want:

if (currentNode === oldNode) {
    continue;
}

We've already processed the old node, yes? So we don't want to process it again. The current code (if I understand the IE bug correctly now) skips the first node after the text node that's replaced, which is potentially a security hole.

@cure53
Copy link
Owner

cure53 commented Sep 14, 2015

That makes much more sense, thanks! I'll add a test-case and then will post the diff here again for review.

@cure53
Copy link
Owner

cure53 commented Sep 14, 2015

@neilj
Copy link
Contributor

neilj commented Sep 14, 2015

That looks good to me.

@cure53
Copy link
Owner

cure53 commented Sep 14, 2015

Thanks, merging...

@cure53
Copy link
Owner

cure53 commented Sep 14, 2015

@luhmann From what I can see we are good to close this issue. Any thoughts?

@luhmann
Copy link
Author

luhmann commented Sep 15, 2015

Fix works perfectly for me. Thank you very much for the quick help.

@luhmann luhmann closed this as completed Sep 15, 2015
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants