{"Author":"Stas","Canonical":"https://corpberry.com/blog/reverse-engineering-12-bot-detectors","Desc":"I studied 12 bot detectors firsthand and built Bot check — a free, open-source, 68-signal bot-detection self-test that shows every signal and exactly how it scored you. Here's what I found.","Feed":"https://corpberry.com/blog/feed.xml","JSONLD":"{\n  \"@context\": \"https://schema.org\",\n  \"@type\": \"BlogPosting\",\n  \"headline\": \"What I learned reverse-engineering 12 commercial bot detectors (and the open-source checker I built from it)\",\n  \"description\": \"I studied 12 bot detectors firsthand and built Bot check — a free, open-source, 68-signal bot-detection self-test that shows every signal and exactly how it scored you. Here's what I found.\",\n  \"datePublished\": \"2026-07-28T00:00:00Z\",\n  \"dateModified\": \"2026-07-28T00:00:00Z\",\n  \"url\": \"https://corpberry.com/blog/reverse-engineering-12-bot-detectors\",\n  \"mainEntityOfPage\": {\n    \"@type\": \"WebPage\",\n    \"@id\": \"https://corpberry.com/blog/reverse-engineering-12-bot-detectors\"\n  },\n  \"author\": {\n    \"@type\": \"Person\",\n    \"name\": \"Stas\",\n    \"url\": \"https://corpberry.com\",\n    \"sameAs\": [\n      \"https://www.linkedin.com/in/stanislav-navarici/\"\n    ]\n  },\n  \"publisher\": {\n    \"@type\": \"Person\",\n    \"name\": \"Stas\",\n    \"url\": \"https://corpberry.com\",\n    \"sameAs\": [\n      \"https://www.linkedin.com/in/stanislav-navarici/\"\n    ]\n  },\n  \"inLanguage\": \"en\"\n}","OGType":"article","Post":{"Slug":"reverse-engineering-12-bot-detectors","Title":"What I learned reverse-engineering 12 commercial bot detectors (and the open-source checker I built from it)","Date":"2026-07-28T00:00:00Z","Desc":"I studied 12 bot detectors firsthand and built Bot check — a free, open-source, 68-signal bot-detection self-test that shows every signal and exactly how it scored you. Here's what I found.","Image":"","Draft":false,"HTML":"\u003cp\u003e\u003cimg src=\"/static/img/botcheck-demo.gif\" alt=\"Bot check scoring a browser in real time, with a per-signal breakdown\"\u003e\u003c/p\u003e\n\u003cp\u003eI run a small collection of tools at corpberry.com. The one I've put the most\ninto is Bot check: a live score of how much your browser looks like a human\nversus an automated bot. It's free, open source, and it shows every signal it\nuses. This post is about why I built it that way, and what I found when I studied\nhow the commercial detectors actually work.\u003c/p\u003e\n\u003ch2\u003eThe thesis: a browser can claim anything\u003c/h2\u003e\n\u003cp\u003eEvery signal a browser reports over JavaScript is spoofable. User-agent,\nplatform, screen size, the \u003ccode\u003enavigator\u003c/code\u003e object, canvas output, WebGL vendor: a\ndetermined script can lie about all of it, and stealth tooling like\npuppeteer-extra-plugin-stealth does exactly that, well.\u003c/p\u003e\n\u003cp\u003eSo trusting what the browser \u003cem\u003eclaims\u003c/em\u003e is a losing game. The detection power is in\nthe cross-check: compare what the browser says against what the connection and\nthe deeper fingerprint actually reveal, and look for the disagreements. A\nheadless browser can tell you it's Chrome on Windows. It's much harder for it to\nmake that claim survive its HTTP header order, its IP reputation, and the way its\nJavaScript engine feature-tests as Blink-versus-Gecko all agreeing.\u003c/p\u003e\n\u003cp\u003eBot check runs 68 tiered checks on exactly that principle. It fuses:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003ea client-side JavaScript fingerprint,\u003c/li\u003e\n\u003cli\u003eserver-observed HTTP headers,\u003c/li\u003e\n\u003cli\u003eand IP reputation (datacenter / VPN / proxy / Tor),\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003ethen cross-checks the three and shows you every signal, why it fired, and how\nmuch it moved the score. Verdict comes out as human, suspicious, bot, or\n\u0026quot;good-bot\u0026quot; for a verified crawler like Googlebot.\u003c/p\u003e\n\u003ch2\u003eWhat the commercial services do well\u003c/h2\u003e\n\u003cp\u003eTo build it I studied 12 public detectors firsthand: CreepJS, FingerprintJS,\nDataDome, BrowserScan, iphey, pixelscan, sannysoft, whoer, AmIUnique, EFF's\nCover Your Tracks, incolumitas, and deviceandbrowserinfo. A few things stood out:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eThe best ones feature-detect the real engine.\u003c/strong\u003e Instead of trusting the UA\nstring, iphey's engine (the open-source MixVisit core) probes for APIs only a\ngiven engine has: \u003ccode\u003ewebkitResolveLocalFileSystemURL\u003c/code\u003e + \u003ccode\u003eBatteryManager\u003c/code\u003e +\n\u003ccode\u003enavigator.vendor\u003c/code\u003e for Chromium, \u003ccode\u003ebuildID\u003c/code\u003e + \u003ccode\u003eonmozfullscreenchange\u003c/code\u003e for\nGecko, \u003ccode\u003eApplePayError\u003c/code\u003e for WebKit — then compares the answer to what the UA\nclaims. Bot check ships its own version of this: one check fingerprints the\nJS VM itself from the \u003ccode\u003eError\u003c/code\u003e stack format (V8's \u003ccode\u003eat\u003c/code\u003e frames vs\nSpiderMonkey's \u003ccode\u003efn@url\u003c/code\u003e frames and proprietary \u003ccode\u003efileName\u003c/code\u003e/\u003ccode\u003elineNumber\u003c/code\u003e). When\nI pointed a browser claiming to be Firefox at it, the real engine stayed V8 —\nand the check fired.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eCreepJS goes deep on lie detection.\u003c/strong\u003e It doesn't just check \u003ccode\u003etoString\u003c/code\u003e for\n\u003ccode\u003e[native code]\u003c/code\u003e; per API function it checks illegal own-properties and\ndescriptors (\u003ccode\u003eprototype\u003c/code\u003e/\u003ccode\u003earguments\u003c/code\u003e/\u003ccode\u003ecaller\u003c/code\u003e), and traps whether calling,\n\u003ccode\u003enew\u003c/code\u003e, \u003ccode\u003eapply\u003c/code\u003e, or class-\u003ccode\u003eextends\u003c/code\u003e throws the correct \u003ccode\u003eTypeError\u003c/code\u003e. The\nsobering part: I built six checks specifically targeting the patches\npuppeteer-extra-plugin-stealth installs — and stealth 2.11.2 evaded all six,\ncleanly, including hiding \u003ccode\u003enavigator.webdriver\u003c/code\u003e in the main thread, iframes,\n\u003cem\u003eand\u003c/em\u003e Service Workers. What caught it instead was three boring cross-context\nconsistency checks (UA, CPU cores, and WebGL renderer disagreeing between\ncontexts) — enough for a 25/100 \u0026quot;bot\u0026quot;. Then it got better: a single illegal\ncall, \u003ccode\u003eFunction.prototype.toString.call(null)\u003c/code\u003e, leaked stealth's Proxy\nthrough a raw stack frame, because a recent V8 stack-format change silently\nbroke stealth's own stack-stripping logic. With that check in, stealth's\nscore dropped from 25 to 0.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eThe unforgeable layer is the network.\u003c/strong\u003e The edge vendors cross-check the\nTLS/TCP/HTTP2 handshake against the claimed browser. That's the one class of\nsignal a JavaScript spoofer can't touch from the page. It's also the one I\nstructurally can't see: Cloudflare terminates the visitor's TLS at its own\nedge and opens a \u003cem\u003eseparate\u003c/em\u003e connection to my origin, so the ClientHello my\nserver sees is Cloudflare's, identical for every visitor. (Cloudflare will\nsell you the real fingerprint as a header — gated behind Enterprise Bot\nManagement, which is a steep price for a personal tool.) I say so plainly on\nthe page instead of pretending otherwise.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eHonesty is a feature.\u003c/strong\u003e deviceandbrowserinfo states outright that its verdict\ndoesn't use IP or behavior; incolumitas warns \u0026quot;false positives are expected.\u0026quot;\nThat candor is what makes a checker trustworthy as a reference.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2\u003eWhat it catches in practice\u003c/h2\u003e\n\u003cp\u003eI ran the real thing against five automation setups. Playwright headless\nChromium: 0/100 — \u003ccode\u003ewebdriver\u003c/code\u003e, the \u003ccode\u003eHeadlessChrome\u003c/code\u003e UA, and the SwiftShader\nsoftware renderer gave it away. Selenium + chromedriver: 0/100 — plus\nchromedriver's classic \u003ccode\u003e$cdc_...\u003c/code\u003e global markers, all seven of them. The\nhumbling one was a hand-rolled raw-CDP Chromium with no automation flags: 40/100,\ncaught almost entirely by its \u003ccode\u003eHeadlessChrome\u003c/code\u003e user-agent while every\narchitectural check read clean. A disciplined custom client evades nearly\neverything client-side, and I've documented that as an accepted gap rather than\npretending the score is magic.\u003c/p\u003e\n\u003ch2\u003eWhy open source, why transparent\u003c/h2\u003e\n\u003cp\u003eMost polished checkers are either closed demos for a paid product or opaque about\nhow the number is computed. I wanted the opposite: a checker you can read. The\nscorer is open source, every signal is shown with a plain reason, and the repo\nincludes my writeups of all 12 services. A low score means \u0026quot;looks automated,\u0026quot;\nnot \u0026quot;is malicious,\u0026quot; and privacy-hardened or VPN users will score lower by design.\nIt stores nothing but a one-way hash of the stable fingerprint plus IP, on a\n30-day rolling window, purely to spot the same fingerprint reused across many\nIPs. It blocks nothing. It's a mirror, not a firewall.\u003c/p\u003e\n\u003ch2\u003eThe boring-but-nice engineering bit\u003c/h2\u003e\n\u003cp\u003eThe whole site, portfolio plus every tool, is one Go binary that routes by\nsubdomain. Server-rendered HTML with htmx and Alpine, and zero Node: Tailwind\nruns via its standalone CLI and the little JS is vendored. Every endpoint\ncontent-negotiates, so every tool is also a curl-able JSON API for free. I'm a\nPython backend dev by day; Go's \u0026quot;one static binary, no runtime\u0026quot; story is why\nthis is one \u003ccode\u003edocker compose up\u003c/code\u003e instead of a stack.\u003c/p\u003e\n\u003ch2\u003eTry it / read it\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003eTool: \u003ca href=\"https://botcheck.corpberry.com\"\u003ehttps://botcheck.corpberry.com\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003ePoint a headless browser at it and watch it get caught, signal by signal.\u003c/li\u003e\n\u003cli\u003eCode + the 12-service research: \u003ca href=\"https://github.com/Landver/site-of-tools\"\u003ehttps://github.com/Landver/site-of-tools\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eI'd genuinely like to know what it flags on your setup, and where it's wrong.\u003c/p\u003e\n"},"Published":"2026-07-28T00:00:00Z","Title":"What I learned reverse-engineering 12 commercial bot detectors (and the open-source checker I built from it) — corpberry.com"}
