CSS-Based Fingerprinting

Avatar of Chris Coyier
Chris Coyier on

Fingerprinting is bad. It’s a term that refers to building up enough metadata about a user that you can essentially figure out who they are. JavaScript has access to all sorts of fingerprinting possibilities, which then combined with the IP address that the server has access to, means fingerprinting is all too common.

You don’t generally think of CSS as being a fingerprinting vector though, and thus “safe” in that way. But Oliver Brotchie has documented an idea that allows for some degree of fingerprinting with CSS alone.

Think of all the @media queries we have. We can test for pointer type with any-pointer. Imagine that for each value, we request a totally unique background-image from a server. If that image was requested, we know those @media queries were true. We can start to fingerprint with something like this:

.pointer {
  background-image: url('/unique-id/pointer=none')
}

@media (any-pointer: coarse) {
  .pointer {
    background-image: url('/unique-id/pointer=coarse')
  }
}

@media (any-pointer: fine) {
  .pointer {
    background-image: url('/unique-id/pointer=fine')
  }
}

Combine that with the fact that we can test for a dark mode preference with prefers-color-scheme, the fingerprint gets a bit clearer. In fact, it’s the current draft for CSS user prefer media queries that Oliver is most concerned about:

Not only will the upcoming draft make this method scalable, but it will also increase its precision. Currently, without alternative means, it is hard to conclusively link every request to a specific visitor as the only feasible way to determine their origin, is to group the requests by the IP address of the connection. However, with the new draft, by generating a randomised string and interpolating it into the URL tag for every visitor, we can accurately identify all requests from said visitor.

There are tons more. We can make media queries that are 1px apart and request a background image for each, perfectly guessing the visitor’s window size. There are probably a dozen or more exotic media queries that are rarely used, but are useful specifically to fingerprinting with CSS. Combine that with @supports queries for all sorts of things to essentially guess the exact browser. And combine that with the classic technique of testing for installation of specific local fonts, and you have a half-decent fingerprinting machine.

@font-face {
  font-family: 'some-font';
  src: local(some font), url('/unique-id/some-font');
}

.some-font {
  font-family:'some-font';
}

The generated CSS to do it is massive (here’s the Sass to generate it), but apparently it’s heavily reduced once we can use custom properties in URLs.

I’m not heavily worried about it, mostly because I don’t disable JavaScript and JavaScript is so much more widely capable of fingerprinting already. Plus, there are already other types of CSS security vulnerabilities, from reading visited links (which browsers have addressed), keylogging, and user-generated inline styles, among others that folks have pointed out in another article on the topic.

But Oliver’s research on fingerprinting is really good and worthy of a look by everyone who knows more about web security than I do.