LavaMoat / snow

Use Snow to finally secure your web app's same origin realms!
https://lavamoat.github.io/snow/demo/
MIT License
100 stars 9 forks source link

Bypass with multiple doc.write calls #116

Closed mmndaniel closed 1 year ago

mmndaniel commented 1 year ago
var f = document.createElement('iframe');
document.body.appendChild(f);
f.contentDocument.write('<iframe id="tst');
f.contentDocument.write('"></iframe><script>tst.contentWindow.alert(1);</script>');

What happens is that document.write calls are buffered, but handleHTML sees only one chunk at a time so it won't find anything inside the template.

mmndaniel commented 1 year ago

related: bypass with multiple arguments to doc.write

var f = document.createElement('iframe');
document.body.appendChild(f);
f.contentDocument.write('<iframe id="tst', '"></iframe><script>tst.contentWindow.alert(1);</script>');
serapath commented 1 year ago

wow, mental.

Given there is https://github.com/LavaMoat/snow/blob/48ec98b16b2d91c4f2535d78ae0597bb7e636201/src/inserters.js#L33 to deal with .innerHTML, could the same be applied here by hooking into contentDocument.write?

Basically this snippet fixes it and maybe can be generalized.

var f = document.createElement('iframe');
document.body.appendChild(f);
fix(f) // should be done by patched `.appendChild`
f.contentDocument.write('<iframe id="tst');
f.contentDocument.write('"></iframe><script>tst.contentWindow.alert(1);</script>');

function fix (f) {
  var old_write = f.contentDocument.write
  var content = ''
  const parser = document.createElement('div')
  f.contentDocument.write = patched_write

  function patched_write (...args) {
    content += args.join('')
    parser.innerHTML = content
    const iframes = [...parser.querySelectorAll('iframe')]
    if (iframes.length) {
      f.contentDocument.close()
      iframes.forEach(iframe => {
        var [s1, s2] = [
          `console.log('apply snow to iframe')`,
          `tst.contentWindow.alert = (...args) => console.log(...args)`
        ].map(s => Object.assign(document.createElement('script'), { textContent: `${s}` }))
        iframe.before(s1)
        iframe.after(s2)
      })
      console.log('fuck')
      console.log(parser.children)
      old_write.apply(f.contentDocument, [parser.innerHTML])
    } else {
      old_write.apply(f.contentDocument, args)
    }
  }
}
mmndaniel commented 1 year ago

@serapath yes, something like that, the main points are that args should be joined (to address this), and buffered (to address the OP). I'd say the optimal parser for this case would be another document (new DOMParser().parseFromString('...', 'text/html') with the same readyState and content as the target document, because if reflects the actual resulting DOM the best. To illustrate my latest point:

var doc1 = new DOMParser().parseFromString('<html><head><body><frameset><frame src="/" /></frameset></body></head><html>', 'text/html');
var doc2 = new DOMParser().parseFromString('<html><frameset><frame src="/" /></frameset><html>', 'text/html');
// compare doc1 and 2

I found it the hard way when an attempted fix broke a similar test...

weizman commented 1 year ago

yes, and https://github.com/LavaMoat/snow/issues/80#issuecomment-1628413209

weizman commented 1 year ago

Sorry to ruin the party guys, but with the help of #118 we might not need to dig too deep into this (thanks for the help!)