Technical · May 21, 2026 · 6 min read

Faking browsers is easy — but not at scale

We watched 131,819 residential US IPs throw 1.4M brute-force attempts at one mobile-only customer over 30 days, and they all collapsed onto a handful of fingerprints.

Faking browsers is easy — but not at scale

One of our customers runs a mobile-only product. Their users authenticate from iOS and Android apps and that is the only way to use the service. So when we started seeing Chrome on Windows showing up against their endpoint at six-figure daily volume, we knew the whole stream was automated. Over the last 30 days we logged 1,476,767 attack requests against this single customer, riding 131,819 distinct residential IP addresses.

This post is a quick look at what those requests actually looked like once we had them all in one place, and what the shape of a modern account-takeover campaign tells us about how attackers are spending money these days. We've also published the full IP list as a free download for anyone who wants to feed it into their own blocklists or research.

Renting tens of thousands of residential IPs has become cheap. Renting tens of thousands of genuinely different browser environments has not. That gap is the thing that gives attackers like this away.


The shape of the wave

Because the customer's real users come only from native mobile apps, the heuristic for separating attack from legitimate is unusually clean: is the request Chrome on Windows? If yes, automated. We ran that filter over 30 days of the powcaptchas collection and got this:

Daily volume of legitimate mobile-app traffic versus Chrome-on-Windows attack traffic over 30 days, with several large attack spikes
Traffic30-day volume
Legitimate mobile-app requests20,933
Attack requests (Chrome on Windows)1,476,767
Distinct attacker IP addresses131,819
Peak attack day (2026-05-18, 24 hours)225,002

The attack outweighed legitimate traffic by roughly 70 to 1. Long quiet days, then weekend-sized spikes that pushed past 200,000 requests on a single day, with four separate days above 150,000.


Where the IPs came from

The interesting question for us was who the attacker was paying for those 131,819 IPs. We pulled the full IP list out of the database, fed it into our own local ipinfo service, and aggregated by ASN, country, and IP type. Out of the 128,793 IPv4 addresses we could resolve, the picture was very consistent:

Horizontal bar chart: 89.4% of attack traffic comes from residential ISP ASNs, 7.3% from hosting, 2.1% business, tiny slivers from government/education/banking
CategoryIPsHitsShare of attack
Residential / mobile ISP117,1411,244,63289.4%
Datacenter8,695118,1768.5%
VPN7,574103,3647.4%
Public proxy3,71033,5862.4%
Known abuser networks7,24962,0464.5%
Tor000

Categories overlap (an IP can be both VPN and abuser), but the headline is clear enough: about nine in ten requests arrive from real broadband customers' IP addresses. The attacker is paying for a residential proxy pool, which is a meaningful capital outlay - pools at this scale go for several thousand dollars a month - rather than spinning up cheap datacenter exits.

The other striking number is the geography. 125,202 of 128,793 distinct attacker IPs (97.2%) geolocated to the United States. Everything else is a long tail of Canada, the UK, the Caribbean, a smattering of South American and European IPs. This is a US-focused operation buying a US-shaped proxy footprint.

When you break out the top ASNs the operator has clearly bought wide coverage of every major US broadband and mobile carrier:

Top 12 ASNs by attack volume: Comcast, Verizon, T-Mobile, Cox, AT&T, Charter, Frontier, Cablevision, Charter regionals, Verizon Cellco, Charter Carolinas, Charter Midwest
RankASNNetworkAttack hitsShare
1AS7922Comcast (US)304,36821.9%
2AS701Verizon Business / UUNET (US)100,0477.2%
3AS21928T-Mobile US86,4746.2%
4AS22773Cox Communications (US)68,2244.9%
5AS7018AT&T (US)65,0154.7%
6AS20115Charter Communications (US)51,5913.7%
7AS5650Frontier (US)41,9183.0%
8AS6128Cablevision / Optimum (US)38,8522.8%
9AS20001Charter (TWC PacWest, US)36,4992.6%
10AS6167Verizon Cellco (US)35,8282.6%

The implication for anyone hoping to defend with a reputation blocklist is uncomfortable. A list that blocks datacenters, known VPNs, and Tor stops less than 11% of this campaign by volume. The rest are real Comcast and T-Mobile customers - or rather, machines exiting through them, which from the network's perspective is the same thing. You cannot block Comcast.

If you would like to use the list for your own research, the full IPv4 list is here:


You can't fake what you can't see

If reputation lists are mostly off the table, the question is what else gives the attacker away. Every HTTPS request carries quite a lot of information beyond the URL and the headers a developer typically thinks about:

  • TLS ClientHello / JA4 - the order of cipher suites, the supported extensions, ALPN, signature algorithms. Real Chrome on Windows emits a measurably different ClientHello to Firefox, to a Go HTTP client, to curl-impersonate, or to a headless Chromium with a non-default config. JA4 is a canonical hash of that.
  • Request header shape - not just what headers are sent, but which ones are sent and in what order. Real Chrome on Windows sends sec-ch-ua, sec-ch-ua-mobile, sec-ch-ua-platform, priority and so on. A program that pretends to be Chrome generally does not.
  • Accept-Language - real humans living in different places carry different distributions of locales and quality-factor lists. Automated traffic carries one.
  • sec-ch-ua-platform - trivial to lie about on its own, but the lie is only useful if it stays consistent with all the other signals.

Faking any of these for a single request is easy. The harder problem for the attacker is making each one of their tens of thousands of containers look genuinely different from the next - different TLS stack, different header ordering, different locale, a UA that matches the platform header, a platform header that matches the JA4, and so on across every dimension at once. We see plenty of campaigns where the attacker has worked hard on a single forged request and then deployed that same exact request from 50,000 IPs.

This campaign is one of those. Here's what 1,476,767 attack requests look like when you ask "how many distinct values do we see for each signal?":

Bar chart: 131,819 attacker IPs collapse to only 14 User-Agent strings, 26 Accept-Language values, 8 JA4 TLS fingerprints, and 7 distinct header shapes
Signal across the 1.4M attack requestsDistinct values
Distinct attacker IP addresses131,819
User-Agent strings14
Accept-Language values26
JA4 TLS fingerprints8
sec-ch-ua-platform values1
Full request-header shapes7

One JA4 fingerprint covers 98.4% of all attack TLS handshakes and rides 130,991 of the 131,819 IPs. Three User-Agent strings (Chrome 146, 147 and 148 on Win64) account for 99.7% of attack requests. The 130,000 residential IPs are wearing the same handful of outfits.


What real traffic looks like

You might suspect that low diversity is normal for this customer. Maybe everyone using the service happens to run one phone in one locale and that's just the shape of the userbase. It isn't. In the same 30-day window the legitimate mobile-app traffic carries far more environmental variety:

Side-by-side comparison: attacker traffic has only 8 JA4 fingerprints and 14 User-Agent strings, while the original Approved set carried 26 JA4 fingerprints and 2,473 User-Agent strings
SignalAttackerLegitimate (Approved set)
Distinct JA4826
Distinct User-Agent142,473

Real users are running iOS 17, 18 and 26 across half a dozen iPhone generations, Android on every OEM under the sun, WebViews from social apps, the occasional desktop visit, Brave / Safari / Firefox here and there. The attacker is running three Chrome-on-Windows User-Agents on top of one TLS stack.


Why this is expensive to fix from the attacker's side

You might be wondering why the attacker doesn't simply rotate more fingerprints. There's a real cost asymmetry here that's worth spelling out.

  • Residential proxy bandwidth runs at roughly $1-$10 per GB. 131,000 IPs delivering 1.4M small requests is a few thousand dollars of traffic. That's already what the attacker is paying.
  • A real, distinct browser environment - one that holds together across TLS, headers, locale, behavioural fingerprint, and the rest - is much more expensive to provision. Either you compromise actual end-user devices (malware) or you orchestrate full Chromium instances with real GPU drivers, fonts, timezones, audio stacks, randomised hardware fingerprints, and per-instance OS configuration. The container approach typically costs $30-$100 per machine per month for the kind of fleet that produces convincing diversity. Even 5,000 of those is a serious budget. 131,000 of them is not in the same universe as the proxy bill.
  • Open-source tooling like curl-impersonate and stock HTTP libraries is free, but each one emits a small, fixed set of TLS fingerprints. We can't tell from a single hash which library is in use, but a giant residential pool collapsed onto one fingerprint is what you'd expect from this class of tool, not from a real browser fleet.

So the attacker faces a choice between cheap IPs with poor environment diversity, or rich environment diversity that costs orders of magnitude more to run. The campaign we're looking at has clearly picked the first option.


How we caught them

We don't write a hand-crafted rule per attacker. Our detection pipeline ingests the fingerprint signals above and learns the signatures of malicious clusters automatically, and once a cluster is identified it's blocked at the access policy layer before our CAPTCHA is even served. For this campaign that means almost a million attack requests have been intercepted by the time the rule we're describing learned what to look for, with many more sitting blocked in the day-by-day chart above.

Concretely, for every blocked attempt:

  • The attacker paid residential proxy bandwidth for a request that never reached the challenge.
  • The customer's application never saw the request either - we intercepted it before it left our edge.
  • Our cost-per-blocked-attempt is a single classifier evaluation. The attacker's cost-per-blocked-attempt is their proxy bandwidth plus their list-management overhead.

We're not naive enough to think attackers can't adapt. They can, and they will. But every adaptation costs them something, and the longer the campaign runs, the more obvious the residue becomes. The point of publishing the specifics here, IP list and all, is to make the next adaptation just a little bit more expensive.


What this tells you about your own traffic

If you run an auth, signup, payment, or account-recovery endpoint at any meaningful scale, you are very likely seeing campaigns shaped like this whether you can see them or not. The shape is consistent across the dozens we've watched this year:

  1. A large residential proxy pool, often hundreds of thousands of IPs strong, spread across every major consumer ISP in the target country.
  2. A short list of fake credentials or stolen identities being cycled through it.
  3. A very small set of TLS, header and User-Agent fingerprints riding underneath all of it.
  4. Bursty activity - long quiet days punctuated by weekend spikes.

Rate-limiting by IP catches a fraction. Blocklisting proxy and VPN ranges catches single-digit percentages on top. What works against a campaign of this shape is the layer the attacker can't cheaply diversify: TLS stack, header shape, runtime environment, behavioural fingerprint.

That's the layer Prosopo's bot detection operates on, and it's the layer we'll keep publishing about as new campaigns come through. If you'd like a second pair of eyes on your own login traffic, get in touch.

Tagged

bots bot-detection technical security account-takeover brute-force tls ja4 fingerprinting
Chris Taylor

Chris Taylor

Building privacy-first bot protection at Prosopo.

More articles by Chris Taylor

Related Posts to Faking browsers is easy — but not at scale

How to deploy Procaptcha on your website or app

How to deploy Procaptcha on your website or app

Wed, 20 Sept 2023

TypeScript: Mapped Type Magic 🪄

TypeScript: Mapped Type Magic 🪄

Wed, 13 Mar 2024

Vite: How to handle `.node` files

Vite: How to handle `.node` files

Mon, 08 Apr 2024

Using Vite To Rebuild Local Dependencies in an NPM Workspace

Using Vite To Rebuild Local Dependencies in an NPM Workspace

Thu, 18 Apr 2024

TypeScript: Branded Types 🔧

TypeScript: Branded Types 🔧

Mon, 22 Apr 2024

actions/cache@v3 restore: GitHub Actions Cache Fix

actions/cache@v3 restore: GitHub Actions Cache Fix

Tue, 18 Jun 2024

tsx vs ts-node: Time to Drop ts-node for tsx

tsx vs ts-node: Time to Drop ts-node for tsx

Wed, 26 Jun 2024

How to Test Your Ansible Playbooks Locally

How to Test Your Ansible Playbooks Locally

Tue, 30 Jul 2024

How Does CAPTCHA Collect User Data? The Reality

How Does CAPTCHA Collect User Data? The Reality

Fri, 25 Apr 2025

Monorepo vs Multirepo Architecture: How to Decide?

Monorepo vs Multirepo Architecture: How to Decide?

Thu, 01 May 2025

Mastering Versioning: A Guide to Software Stability

Mastering Versioning: A Guide to Software Stability

Fri, 02 May 2025

Independent vs Locked Versioning in a Workspace

Independent vs Locked Versioning in a Workspace

Sat, 03 May 2025

Streamlining Releases: Building Scalable CICD Pipelines with Changesets

Streamlining Releases: Building Scalable CICD Pipelines with Changesets

Tue, 06 May 2025

PHP Views Package - Templating Made Easy with Blade and Model-Driven Approach

PHP Views Package - Templating Made Easy with Blade and Model-Driven Approach

Mon, 19 May 2025

Reactive Frameworks Cheatsheet - React, Vue, Svelte and Angular

Reactive Frameworks Cheatsheet - React, Vue, Svelte and Angular

Mon, 19 May 2025

We cut our Mongo DB costs by 90% by moving to Hetzner

We cut our Mongo DB costs by 90% by moving to Hetzner

Wed, 12 Nov 2025

We suffered MongooseJS so you don't have to

We suffered MongooseJS so you don't have to

Tue, 02 Dec 2025

How to Add CAPTCHA to Fastly CDN

How to Add CAPTCHA to Fastly CDN

Tue, 20 Jan 2026

How to Stop PHP Form Spam (Without a Plugin)

How to Stop PHP Form Spam (Without a Plugin)

Wed, 24 Jun 2026

Why am I clicking traffic lights? The reason Google reCAPTCHA frustrates users

Why am I clicking traffic lights? The reason Google reCAPTCHA frustrates users

Fri, 15 Mar 2024

How to Integrate CAPTCHA Without Violating User Rights

How to Integrate CAPTCHA Without Violating User Rights

Sat, 12 Apr 2025

Procaptcha vs Friendly Captcha - Why Real Bot Protection Matters in 2026

Procaptcha vs Friendly Captcha - Why Real Bot Protection Matters in 2026

Wed, 13 Aug 2025

Why is Ticket Scalping so Hard to Stop? Uncovered

Why is Ticket Scalping so Hard to Stop? Uncovered

Sat, 15 Mar 2025

🎫 Preventing Ticket Bots - The Battle for Fair Access

🎫 Preventing Ticket Bots - The Battle for Fair Access

Sun, 02 Nov 2025

How to Stop Ticket Scalping: The 2026 Anti-Bot Playbook

How to Stop Ticket Scalping: The 2026 Anti-Bot Playbook

Mon, 29 Jun 2026

Ready to ditch Google reCAPTCHA?

Start for free today. No credit card required.