april
Bot detection is dead, long live bot detection: reverse-engineering Kasada in an afternoon
Learn why, as agents blur the line between humans and automation and the economics of reverse‑engineering bot detection shift, verified identity is becoming the future.
bot detection has been a central part of our industry. it’s how the internet policed itself. it decided which signups were real, which login attempts were worth a second look, which ad impressions were safe to buy, and which purchases to approve. underneath all the vendors and acronyms was a simple assumption: humans on the internet are good; automation—associated with inventory hoarding, fake accounts, scraping, and fraud—is bad. agents broke that assumption. once you have agents that can go off and do work on your behalf, the line between “user” and “bot” blurs.
on top of that, today’s bot detection systems are built for a different adversary. vendors have spent years building increasingly sophisticated client-side fingerprinting: bytecode vms, obfuscated string tables, encrypted telemetry, crash-and-capture integrity probes. the underlying bet was straightforward: complexity buys time. deobfuscation is slow, tedious work, so by the time someone reverse-engineers your sensor script, you have already shipped a new one. agents have changed the economics of reverse-engineering. we recently went digging into why twitch’s kasada integration was intermittently rejecting automation tokens from an otherwise clean browser of ours. in a matter of hours, one of our security engineers, armed with an agent, managed to reverse-engineer the bot detection.
agents broke both halves of the old world: the overly simplistic assumption that "humans good, automation bad," and the comfortable lie that "complexity buys you time." what comes next won't look like a binary filter in front of your site, it will look more like verified identities for a mixed population of humans and agents, where actors can prove who they are.
to make this argument less theoretical, we’re sharing exactly how trivial it was to reverse engineer kasada’s bot detection. but along the way, the trail wound through quite a gnarly series of events involving a 449 kb bytecode vm, xor-encrypted telemetry (with a meme for a key), and finally a dive into chromium’s c++ internals. at every step, agents took what used to be days of manual work and compressed it into hours.
getting our first clue
twitch uses kasada for bot detection. many vendors rely on behavior signals like mouse movements, keyboard timing, or rendering fingerprints (canvas hashes, css probes). kasada is different. kasada’s primary weapon is a bytecode virtual machine (vm) embedded in javascript. this vm runs inside the browsers, executes environment integrity checks, and produces a cryptographic token that must accompany every subsequent request. if the token fails server-side validation, you’re out. the important twist: kasada doesn’t care much what your browser looks like; it cares whether the javascript engine, the dom, and the protocol layer all tell the same story.
our first experiment was simple: remove playwright entirely. same chromium, same twitch page, same kasada script. we connected over raw cdp with a bare websocket and three messages, no libraries, no abstractions. every token we captured passed kasada’s integrity checks.
then we repeated the flow with playwright wired up. same browser, same network, and suddenly a significant chunk of tokens started failing. the only thing we changed was how we talked to the browser. the problem wasn’t the fingerprints, it was something leaking out of the protocol layer between playwright and chromium.
so what was playwright doing that kasada could see?
deobfuscating 449kb of javascript
to understand what kasada checks, we needed to understand its code. kasada’s ips.js is a 449kb file built around a custom bytecode vm with multiple layers of obfuscation: a 437kb encoded string table, time-based seeds, integrity checksums, custom alphabet encoding, and per-site variants (twitch uses different parameters than other kasada-protected sites).
a year ago, this is where the investigation would have stalled for days. deobfuscating a custom bytecode vm means understanding the encoding scheme, reimplementing the decoder, and then making sense of thousands of extracted strings. it’s tedious, error-prone work that requires a security researcher who’s seen enough obfuscation patterns to recognize what they’re looking at.
instead, we fed chunks of ips.js to an agent. within minutes, it identified the string table structure as a custom radix decoder with a rotating alphabet, explained how twitch’s variant differs from other kasada sites, and generated a working extractor. we ran it and pulled 1,515 decoded strings: the full inventory of everything kasada’s vm probes, extracted in one pass. browser apis like navigator.webdriver and screen.width, deeper checks like rtcpeerconnection and getbattery, canvas fingerprinting primitives, named bot detection rules like bot606 and bot1375_mfsk, even competitor captcha detection.
this told us what kasada looks at. but not why our tokens were failing. we needed to see the actual detection results, the verdict kasada was reaching about our browser.
decoding reports with omgtopkek
watching network traffic, we noticed periodic post requests to https://reporting.cdndex.io/error, kasada’s telemetry endpoint. each payload was a json object containing a single base64-encoded blob that decoded to gibberish. it was encrypted. they all started with similar byte patterns. if the plaintext was structured data, say json, they’d all likely share a prefix. that was enough to try a known-plaintext attack. we guessed the plaintext began with {"type":", xored those nine bytes against the ciphertext, and out popped: omgtopkek.kasada was using a repeating 9‑byte xor key, and the key was a meme. we applied omgtopkek across the entire payload and got clean, readable json back. suddenly, we were reading kasada’s sensor data in plain english. the decoded reports were surprisingly verbose. they contained a diagnostic dump of every integrity check kasada runs: cross-frame consistency, user-agent validation, navigator.webdriver getter integrity, window property leak detection, all of it annotated. all passing.
the most interesting entries were the crash-and-capture probes. kasada deliberately provokes typeerrors and then inspects the resulting error objects. it’s not checking whether they fail. it’s watching how they fail: message text, stack trace, etc., every quirk that a real browser produces without thinking. in our tests, our browsers threw the right errors every time. the fingerprints were clean. the cross-frame checks were clean. kasada’s probes were all passing. yet something about the playwright runs still smelled wrong to kasada.
we took the decoded reports and asked an agent to diff passing sessions against failing ones. it flagged the crash-and-capture fields as the likely discriminator. the error shapes were slightly different when playwright was connected, even though the fingerprint checks were identical.
patching chromium
at that point we needed to look under javascript. if kasada’s vm was detecting a subtle change in v8’s internal state under playwright, any instrumentation inside javascript would just become another thing kasada could step on.
so we patched chromium instead. we wrote a c++ patch that adds logging to blink’s getter methods for navigator, screen, webgl, canvas, matchmedia, and performance. the observer lives below v8, inside the browser engine. there is no function to tostring(), no property to enumerate, no timing side-channel. from javascript’s perspective, it doesn’t exist.
here again, agents acted as an accelerator. blink’s codebase is enormous, millions of lines of c++. normally, finding the right getter methods to instrument means hours of code archaeology. instead, we described the goal—log every access to navigator, screen, and webgl properties from any frame, including iframes, at the blink c++ layer—and it pointed us to the exact source files and method signatures. the patch took an afternoon instead of a week.
the observer revealed fascinating behavior from kasada’s iframe probes: binary searches over screen dimensions via matchmedia, probing color-gamut, dynamic-range, forced-colors, and a dozen other media features from the iframe context and cross-checking against the main frame. but the observer logs were identical between raw cdp and playwright sessions. whatever playwright was doing differently, it wasn’t changing the browser’s observable properties.
what we found in a couple hours
by the end, the investigation revealed two distinct detection layers for kasada.
the first is protocol-level. when playwright calls connectovercdp, it sends initialization commands that modify v8’s inspector state. these aren’t visible as browser properties, but they change the shape of javascript errors that kasada’s crash-and-capture probes inspect. switching to raw cdp helped reduce failures.
the second is canvas fingerprinting: drawing an image in the browser and hashing the rendered pixels to verify the gpu is real. software-rendered browsers produce pixel hashes that don’t match any consumer gpu, and no amount of api spoofing changes what the renderer actually draws.
verified identities > bot detection
if a small team with off‑the‑shelf agents can deobfuscate your client‑side javascript in an afternoon, the security value of obfuscation is already trending toward zero. the future of defending the internet is not better detection tricks. it is verified identity.
that shift is better for everyone. website operators get reliable signals about who is accessing their services instead of probabilistic guesses. agent developers get a legitimate path that does not involve reverse-engineering sensor scripts. and users stop getting caught in the crossfire of false positives from increasingly aggressive bot detection.
if you want to collaborate on the future of agent identity, reach out to partnerships@kernel.sh