Most people believe that their browsing history is private. However, trackers on websites have clever ways of uncloaking other sites you’ve visited. When sites use such tricks to invade your privacy, security engineers call it “history sniffing”.
There are a few ways that sites can make this happen:
- Using the
:visibility
psuedo-class in CSS. Browsers try to stop this, but it still works. - Shared cookies across sites, like Google’s Adsense. However, this requires multiple sites to cooperate, and only gives you anonymized metadata, not the history of specific individual users.
- Measure the speed at which a user can load a resource from a site, and based on the speed, divine whether it’s cached or not (and thus, whether they’ve visited that site)
The last technique is effective, but difficult to implement. Also, since network requests can be slow or quick for other reasons, it’s not the most reliable approach.
However, explain that there’s still a workaround to make this vulnerability work (link to research by Jes on Arxiv[!] with cool original name for attack. Selective Anchor Visibility Sniffing, eg), we’ll use it since it’s by far the simplest.
How CSS browser history sniffing works
Back in the day, you could use CSS’s visited
pseudo-class to see selectively apply styles depending on whether a page appeared in a user’s browser history. Then, you’d use JS to query whether those styles applied. Thus, you could see whether a user had visited any given site.
Since then, browsers have tried to patch this by preventing you from querying the visited
psuedo-class from JS. Additionally, they restrict styles usable with visited
to only color. That way, you can’t conditionally render an image to see if the visited
style applied.
However, we can still exploit this by changing the color of text to be invisible if a link appears in their history. Then we can see which link they click, assume that link is visible to them, and therefore that they’ve been to that site. Viola!
Using selectively rendered links to sniff history is called Selective Visibility Sniffing.
Of course, this assumes they aren’t browsing incognito.
But most users aren’t. Still, we’ll explore this and other tactics for protecting yourself from history sniffing later on.
Creating a proof-of-concept web app
It’s a fascinating idea, but let’s see real code that exploits this. We’ll make a web app that implements this concept to show how it works in practice. We’ll use First, let’s create our backend that does two things:
- Serves the initial page that will only make links visible if the user has visited certain sites
- Shows us what page was in the user’s history based on what link they clicked
Okay, so let’s create a simple web app using HTML, CSS, and vanilla JavaScript. The web app offers free crypto and have a few links. When clicked, the links will review a site the user has visited to us. We’ll also theme the concept app around crypto, due to its prevalence in low-level cybercrime (see our cryptojacking article for other great examples of this).
<title>History sniffing PoC</title>
<link rel="stylesheet" href="https://www.w3schools.com/w3css/4/w3.css">
<style>
#link {
color: #ccc;
font-weight: bold;
}
#link:visited {
color: black;
}
body,h1 {font-family: "Raleway", Arial, sans-serif}
h1 {letter-spacing: 6px}
.w3-row-padding img {margin-bottom: 12px}
</style>
<div class="w3-content" style="max-width:1500px">
<header class="w3-panel w3-center w3-opacity" style="padding:128px 16px">
<h1 class="w3-xlarge">CRYPTO WISHING WELL</h1>
<div class="w3-padding-32">
<div class="w3-bar w3-border" style="padding:20px;">
<h2>Free money!</h2>
Click a non-greyed option below to claim your cash:<p/>
<a href="https://youtube.com" id="link" class="w3-bar-item w3-button">Bitcoin</a>
<a href="https://reddit.com" id="link" class="w3-bar-item w3-button">Monero</a>
<a href="https://tiktok.com" id="link" class="w3-bar-item w3-button">Doge</a>
<a href="https://example.test" id="link" class="w3-bar-item w3-button">Ethereum</a>
<script>
const cb = e => {
e.preventDefault();
alert(`${e.target.href} is in your history.`);
};
document.addEventListener('click', cb, false);
</script>
Great, now we can load the page and see that the cryptocurrency buttons corresponding to sites we’ve been to before are visible, and the others are black.
Bitcoin and Monero link to Reddit and Youtube, sites I’ve visited. On the other hand, TikTok and example.test isn’t in my history. The web app should be able to detect this when we click the link:
Wonderful! In real life, the button would do whatever normal functionality the user expects. The server would merely log this fact to a tracking database, without the user ever knowing what happened. Black hat marketing – and the worse part is, each time you use the app, you’d reveal a little more about yourself. Because the server would try different links each time to build a unique profile of sites you visit.
Running our app with different browsers
After running this on Chrome, Safari, Firefox, and Brave, the exact same thing happened. The exploit worked on all of them. There is, however, one browser that’s immune: Tor Browser. Let me show you why.
First, we need to allow Tor Browser to access localhost. Due to how Tor Browser works, it can’t do this by default.
Now we can load up our tracking app and see what happens. Remember, if CSS detects a link in our browsing history, it should show up as black instead of gray.
Every link is gray, even though I’ve never been to any of them in Tor Browser! That’s because Tor Browser keeps no history by default. This makes Tor Browser immune to the threat of CSS browser history sniffing.
If you’re worried about privacy and tracking, this (and many other features) make Tor Browser an excellent choice. But protecting from this attack doesn’t require switching to Tor browser. Rather, there are various easy ways to defend against this.
Defending against history sniffing
Browsing with JS disabled would help. Then, we couldn’t easily detect which link was clicked. Although too much of the web uses JS, so this is inconvenient. Another option is to browse without history enabled by default.
This is probably the best way to prevent such an attack, and it’s easy to implement. For example, Firefox makes it easy to disable history: https://support.mozilla.org/en-US/questions/993353.
How browsers could improve
We’ve seen how you can avoid being tracked like this. But most people aren’t privacy fanatics, so they won’t bother with this stuff. To protect them, browsers would have to change how they interpret the :visited
pseudo-class.
Instead of :visited
applying to URLs in the browser’s history, it should only apply to links you’ve clicked while browsing the current site. In other words, if you’ve clicked a link on Google before, that link can still change from blue to purple if it pops up again. However, Reddit needn’t be able to style a link with :visited
just because you clicked that link from Google before.
Another good change would be for browsers to simply not keep history by default. For users who want it, it could be opt-in.