Adbleed: partially de-anonymizing VPN users with adblock filter lists

Most people assume that using a VPN makes them anonymous online. Your real IP is hidden, your traffic is encrypted, and websites see the VPN server’s location instead of yours. We’ve already seen that this is not fully the case thanks to browser fingeprinting techniques such as the many that https://amiunique.org or the EFF can show you. But there’s one more fingerprinting vector that VPNs can’t protect against: the country-specific adblock filter lists installed in your browser. I built a proof of concept that demonstrates this.

You can try it yourself at https://adbleed.eu.

Most AdBlocking base lists only cover English-language ads and major international ad networks. If you’re in Germany, you probably also have EasyList Germany enabled, which blocks German ad networks like adnx.de, adition.de, and adklick.de. If you’re in France, you have Liste FR, which blocks ad6.fr, adaccess.fr, and hundreds of French-specific domains. The same goes for Italy, the Netherlands, Spain, Brazil, Russia, China, Japan, and the Nordics. Most adblockers prompt you to enable your country’s list on install, or enable it automatically based on your locale, such as AdGuard’s Automatically activate language-specific filters feature.

Adblockers like uBlock Origin, Brave’s built-in blocker, and AdBlock Plus all use filter lists: big text files full of rules that tell the browser what to block. The most common one is EasyList, which has over 54,000 domain-blocking rules and is enabled by default in pretty much every adblocker.

These country-specific lists block domains that are not in the base list. That difference is what makes fingerprinting possible. By figuring out which country-specific filter list your browser is using, I know something about where you are or what language you speak, even if you’re routing all your traffic through a VPN exit node in another country. The VPN hides your IP, but it doesn’t change which adblock rules your browser enforces.

Detection method

The detection is entirely client-side by using JavaScript running in the browser, probing whether certain domains are blocked. The technique is simple: try to load a tiny resource (a favicon) from a domain that is blocked only by a specific country’s filter list, and measure how fast the request fails. When an adblocker blocks a request, it intercepts it *before* it reaches the network stack. The image fires its onerror event almost instantly, typically in under 5 milliseconds. When there’s no adblocker blocking the domain, the request actually goes out to the network. Even if the domain doesn’t exist (NXDOMAIN), the DNS lookup alone takes 50-500ms. A TCP connection timeout takes even longer.

That timing difference is the signal.

function probeDomain(domain) {
  return new Promise((resolve) => {
    const img = new Image();
    const start = performance.now();

    img.onerror = () => {
      const timing = performance.now() - start;
      resolve({
        domain,
        timing,
        blocked: timing < 30, // <30ms = adblocker intercepted it
      });
    };

    img.onload = () => resolve({ domain, timing: performance.now() - start, blocked: false });
    img.src = `https://${domain}/favicon.ico?_=${Date.now()}`;
    setTimeout(() => resolve({ domain, timing: 3000, blocked: false }), 3000);
  });
}

For each country, I probe 30 domains. If 20 or more come back as blocked (error in under 30ms), I conclude that the country’s filter list is active. The threshold of 20/30 is deliberately high: different browsers and adblockers bundle different base lists beyond the standard EasyList, and some of those lists may coincidentally block a handful of country-specific domains. A user with the actual country list enabled will easily hit 25-30 out of 30, while a false positive from overlapping base lists will typically land at 2-8. The 20/30 threshold sits comfortably between those ranges.

This research was done using Brave Browser with default Shields settings. I first ran the tool without any country lists enabled to identify which domains Brave’s built-in lists already blocked, then removed those from the probe sets. If you’re testing with uBlock Origin, AdBlock Plus, or another adblocker, your false positive baseline may differ since they each ship different default lists.

Compiling the signature lists

The hardest part is making sure the domains being probed are actually unique to a country’s list and not in the base EasyList. If ad.doubleclick.net is in my probe set, every adblocker user would trigger it, regardless of country lists.

I wrote a script that:

1. Fetches the base EasyList and extracts all 54,415 ||domain^ blocking rules

2. Fetches each country-specific list and extracts its domain rules

3. Subtracts the base EasyList set from each country set

4. Prioritizes domains with country-code TLDs (de, fr, it, etc.)

Before running the country probes, I run two sets of controls:

Positive controls: domains that are in the base EasyList, like pagead2.googlesyndication.com and ad.doubleclick.net. If these aren’t blocked, the user doesn’t have an adblocker and the results won’t be meaningful.

Negative controls: domains that should never be blocked, like www.google.com and cdn.jsdelivr.net. If these are blocked, something weird is going on and the results can’t be trusted.

The https://adbleed.eu proof of concept site tests 7 country-specific lists in sequence. For each list, it probes 30 domains in parallel batches and shows you in real time which ones are being blocked and how fast. After the scan, you get a summary of which country lists were detected, a confidence score, and a detailed breakdown of every blocked domain with its timing.

Privacy implications

This is a real fingerprinting vector. It works through VPNs, through Tor Browser, and through any proxy. It requires no special permissions, no cookies, no server-side cooperation. It’s pure client-side JavaScript.

What an attacker learns:

  • Which country-specific adblock lists you have enabled, which strongly correlates with your country of residence or native language
  • Whether you use an adblocker at all (from the control checks)
  • Combined with other fingerprinting signals (timezone, keyboard layout, fonts, screen resolution), this significantly narrows down who you are

The mitigation is awkward: either don’t use country-specific filter lists (which means more ads in your language get through), enable a few random ones (good idea!), or don’t use an adblocker at all (which is worse for privacy in other ways).

Takeaway

Adblock filter lists are a privacy trade-off. They improve your browsing experience by blocking ads and trackers, but the country-specific lists leak information about your identity that a VPN can’t hide. The fingerprinting technique is trivial to implement (a few hundred lines of JavaScript), hard to detect (it looks like normal failed image loads), and hard to mitigate without giving up the adblocking that makes the web usable.

If you’re working on an adblocker: consider whether country-specific lists could be applied more carefully, perhaps by only activating rules on matching first-party domains rather than globally. If you’re a user who cares about anonymity: be aware that your adblock configuration is part of your fingerprint.

Links

https://easylist.to/

https://amiunique.org/

https://support.brave.app/hc/en-us/articles/6449369961741-How-do-I-manage-adblock-filters-in-Brave

https://adguard.com/kb/general/ad-filtering/adguard-filters/

Sponsor: check out https://hadrian.io for agentic pentesting across your external attack surface


Posted

in

by

Tags: