DEV Community

Cover image for Why Chrome's JA3 is useless now (and what to use instead)
North Dev
North Dev

Posted on

Why Chrome's JA3 is useless now (and what to use instead)

If you've ever tried to make an HTTP client "look like Chrome" to get past a WAF, you've probably copied a Chrome JA3 hash from somewhere, hard-coded it, verified it once against a reflector, and shipped. And then, weeks later, you got blocked anyway — with the exact fingerprint you "matched."

Here's what happened, and what to do about it.

Your TLS handshake talks before your HTTP does

Before a single byte of your HTTP request goes out, your TLS ClientHello is already on the wire: the version you offer, your cipher list, your extensions, your curves and signature algorithms. A CDN reads that and forms a fingerprint. If it says "Go stdlib" or "python-requests" instead of "Chrome," you can send the most perfect browser headers in the world and still eat a 403.

  • JA3 (Salesforce, 2017): an MD5 over five ClientHello fields.
  • JA4 (FoxIO, the modern successor): a structured, human-readable fingerprint — and it sorts the extension list before hashing.

That sorting is the whole story.

Chrome's JA3 is non-deterministic on purpose

Since Chrome 110 (2023), Chrome randomizes its TLS extension order on every connection — a deliberate anti-ossification measure. The side effect: Chrome's JA3 changes every connection. There is no "the" Chrome JA3. Hard-code one and you've pinned a single permutation real Chrome almost never reproduces — so you look less like Chrome.

Same client, three consecutive handshakes:

run 1: JA3 0a9d85cc...   JA4 t13d1516h2_8daaf6152771_d8a2da3f94cd
run 2: JA3 b7ca61f3...   JA4 t13d1516h2_8daaf6152771_d8a2da3f94cd
run 3: JA3 6910cc00...   JA4 t13d1516h2_8daaf6152771_d8a2da3f94cd
Enter fullscreen mode Exit fullscreen mode

JA3 is all over the place. JA4 is constant — the sort washes out the shuffle. Match JA4, not JA3. (Firefox/Safari/iOS don't shuffle the same way, so their JA3 stays stable — this is a Chrome/Chromium thing.)

The third fingerprint most people miss

Even with byte-perfect TLS, you get flagged by your HTTP/2 fingerprint (the "Akamai h2" string): SETTINGS values, WINDOW_UPDATE, stream priority, and pseudo-header order. Most HTTP libraries never think about frame order, so this is where "I set the right ciphers" setups quietly fail. The real checklist is three numbers: JA4 (TLS), Akamai h2 (HTTP/2), and matching headers — not just a User-Agent.

Matching them in practice

I got tired of re-solving this from both ends, so I put it in one small Go binary, fp (fpkit, MIT):

  • fp get <url> --profile chrome — fetch with a byte-exact Chrome ClientHello + HTTP/2 settings. No headless browser, no CGo.
  • fp inspect — a reflector showing a caller's JA3/JA4/JA4H/Akamai-h2 with a plain-English verdict.
  • fp check <url> --profile chrome — a CI guard that fails when your client stops looking like a browser.
  • fp db ja4 <value> — reverse-look-up a JA4 against bundled browser + known-client fingerprints.

Try the reflector with no install:

curl --http2 https://fp.shshbshsnang.shop/api/all
Enter fullscreen mode Exit fullscreen mode

or open https://fp.shshbshsnang.shop/ and it'll tell you whether you read as a browser or a bot.

Takeaways

  1. Don't hard-code a Chrome JA3 — it's non-deterministic by design. Match JA4.
  2. TLS isn't enough; match the HTTP/2 (Akamai) fingerprint too.
  3. Measure against a reflector, and re-measure — fingerprints drift with every browser release.

Honest limit: a careful spoofer can forge any JA4, so an inbound JA4 guard raises cost, it isn't a wall. If you've seen a profile drift, or a CDN keying off something I haven't documented, I'd like to hear it.

Top comments (0)