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.
It gets even scarier with JavaScript thrown in: With the Font Access API, attackers can use a hidden element set in some custom corporate font (eg, Product Sans of Google), and then use the API to do some low-level checks to determine if the font tables match their version Product Sans. That way it is possible to discover if the user is a Google employee. https://web.dev/local-fonts/
Thanks for the great article, I really appreciate it! You missed the permanent hidden cookie bit but the rest of it is covered well.
It’s basically something like a tracking pixel for emails?
Great article. Like most everything else that is multifaceted, fingerprinting isn’t bad per se, it’s all in the how and why it is used.
We all use it to some degree or another most especially in keeping our websites from being compromised or trying to investigate what appears to be illicit intentions.
You don’t even need CSS to use media queries or estimate the viewport. With the
<picture>
element, you can have different images selected based on user preferences (dark/light, reduced-motion, exact viewport size, contrast preferences, etc); with image srcsets, you can get fine-grained information about the display resolution. Server logs will reveal exactly which image was requested, and all you needed was pure HTML.This is why the Tor Browser uses letterboxing to set a fixed viewport size, and encourages users not to customize it. It’s also why users have to choose between anonymity and accessibility.
Another example: lazy loaded images tell servers how far a user has scrolled. Idiosyncratic scrolling behavior can be detected to contribute to a user’s fingerprint; this is why the Tor Browser disables lazy loading in the “safest” mode. I’ve raised an issue to bring some mitigations to the “safer” mode, e.g. by making scrolling increments more coarse.
The Web has failed disabled users who need anonymity. Accessibility preferences are fingerprintable, and little effort has gone into addressing this.