Skip to content
Tech News
← Back to articles

ChatGPT won't let you type until Cloudflare reads your React state

read original more articles
Why This Matters

This article reveals that Cloudflare's Turnstile program not only verifies browser authenticity but also ensures that the user is running a fully booted React application, adding a new layer of bot detection. This sophisticated fingerprinting technique raises privacy concerns and highlights the increasing complexity of user verification methods in the tech industry. For consumers, it underscores the importance of understanding how their browser data is being used for security and anti-bot measures.

Key Takeaways

Every ChatGPT message triggers a Cloudflare Turnstile program that runs silently in your browser. I decrypted 377 of these programs from network traffic and found something that goes beyond standard browser fingerprinting.

The program checks 55 properties spanning three layers: your browser (GPU, screen, fonts), the Cloudflare network (your city, your IP, your region from edge headers), and the ChatGPT React application itself ( __reactRouterContext , loaderData , clientBootstrap ). Turnstile doesn't just verify that you're running a real browser. It verifies that you're running a real browser that has fully booted a specific React application.

A bot that spoofs browser fingerprints but doesn't render the actual ChatGPT SPA will fail.

The Encryption Was Supposed to Hide This

The Turnstile bytecode arrives encrypted. The server sends a field called turnstile.dx in the prepare response: 28,000 characters of base64 that change on every request.

The outer layer is XOR'd with the p token from the prepare request. Both travel in the same HTTP exchange, so decrypting it is straightforward:

outer = json.loads(bytes( base64decode(dx)[i] ^ p_token[i % len(p_token)] for i in range(len(base64decode(dx))) )) # → 89 VM instructions

Inside those 89 instructions, there is a 19KB encrypted blob containing the actual fingerprinting program. This inner blob uses a different XOR key that is not the p token.

Initially I assumed this key was derived from performance.now() and was truly ephemeral. Then I looked at the bytecode more carefully and found the key sitting in the instructions:

[41.02, 0.3, 22.58, 12.96, 97.35]

... continue reading