Beyond OOPSpam: Why Spam Filters Can't Stop AI Bots
OOPSpam filters form spam but can't detect AI scrapers or headless browsers. Learn why behavioral analysis beats spam filters.
securityA technical deep dive into credential stuffing tooling, attack anatomy, and the detection signals that actually work against modern ATO operators.
WebDecoy Team
WebDecoy Security Team
Most write-ups on credential stuffing stop at “attackers replay leaked passwords.” That framing is technically correct and operationally useless. If you actually want to defend a login endpoint, you need to understand what the attack looks like on the wire: what tools generate it, what their configs encode, how they handle MFA and CAPTCHA, and which signals survive the residential-proxy-and-stealth-browser arms race.
This is a hands-on breakdown. We’ll trace a single credential from a paste site to a successful account takeover, look at real config files from OpenBullet 2 and SilverBullet, and then walk through the detection signals that actually work in 2026, not just rate limits.
Credential stuffing isn’t a person at a keyboard. It’s a supply chain.
breach dump (raw combo lists)
↓
combo cleaner (dedupe, normalize, shard)
↓
checker tool (OpenBullet 2, SilverBullet)
↓
proxy provider (residential rotation per request)
↓
target login (your endpoint)
↓
valid combos (verified hits)
↓
account market (resold to ATO operators)Each layer is specialized and commoditized:
email:password or user:password files, sometimes hundreds of millions of lines.@gmail.com, @yahoo.com) so checks can be parallelized.valid:hits files and resell access.The unit economics are brutal: combos cost $5–$20 per million, residential bandwidth runs $3–$15/GB, and a 0.1–0.5% hit rate is profitable when validated streaming, retail, or banking accounts resell for $1–$50 each.
That’s the context. Now the wire.
Pick any modern login endpoint. From the attacker’s side, here’s what one credential check looks like end-to-end.
OpenBullet 2 attacks are driven by .opk config files written in LoliCode: a DSL that compiles to C# blocks. A minimal credential-stuffing config for a site with a POST /api/login endpoint and a CSRF token looks like this:
REQUIRE PROXIES
DATA TYPE = CREDENTIALS
# 1. Hit the login page to harvest the CSRF token + session cookie
REQUEST GET "https://target.example/login"
HEADER "User-Agent: <USERAGENT>"
HEADER "Accept-Language: en-US,en;q=0.9"
PARSE "<input name=\"csrf_token\" value=\"(.+?)\"" LR -> VAR "CSRF"
# 2. Submit credentials
REQUEST POST "https://target.example/api/login"
CONTENT "email=<INPUT.USER>&password=<INPUT.PASS>&csrf_token=<CSRF>"
CONTENTTYPE "application/x-www-form-urlencoded"
HEADER "Origin: https://target.example"
HEADER "Referer: https://target.example/login"
# 3. Classify response
KEYCHECK
KEYCHAIN SUCCESS OR
KEY "Set-Cookie" Contains "session_id="
KEY "<SOURCE>" Contains "\"authenticated\":true"
KEYCHAIN FAIL OR
KEY "<SOURCE>" Contains "Invalid credentials"
KEYCHAIN BAN OR
KEY "<RESPONSECODE>" EqualTo "429"
KEY "<SOURCE>" Contains "captcha"A few things to notice:
KEYCHECK block encodes the entire response taxonomy: success, fail, ban, retry, MFA-challenge, captcha-challenge. Each maps to a different bucket so the operator can post-process.BAN is a routing decision, not an outage. A banned response just rotates the proxy and replays the combo. Your 429 is the attacker’s continue.The same config runs against a proxy list, usually socks5://user:pass@host:port lines. Modern checkers integrate with residential providers via API and pull a fresh IP per request. The IPs:
This is why “block IPs with too many failed logins” stopped working around 2018. The attacker sees one IP per request. You see one request per IP.
Higher-effort campaigns don’t even use raw HTTP. They drive a real Chromium through Puppeteer or Playwright with stealth patches, or use Browser-as-a-Service like Browserbase / Hyperbrowser to outsource the fingerprint problem entirely. From your server, you see:
If your detection stack is “User-Agent + IP rep + rate limit,” the attack is invisible.
Let’s enumerate the defenses most teams reach for first, and the specific reason each one degrades against modern tooling.
| Defense | Why it degrades |
|---|---|
| Rate limiting per IP | One request per residential IP. You’d need to throttle at single-digit-per-IP-per-day to bite, which kills NAT’d users. |
| Rate limiting per account | Effective for targeted brute force, useless for stuffing, each combo is a different account. |
| CAPTCHA on login | Solver services (2Captcha, CapSolver) cost $1–$3 per 1000 reCAPTCHA v2, and AI vision now solves most variants without human-in-the-loop. |
| Geo / ASN blocking | Residential proxy pools cover every country and consumer ISP. Geo-blocking your own US users is the only real outcome. |
| Block known bad UAs | Configs randomize UAs from a curated pool of real Chrome/Firefox strings. |
| MFA | Helps a lot, but doesn’t help validation. Attackers still confirm valid:hits, then sell the credential to phishers who run MFA-bypass kits (Evilginx, Tycoon). |
None of these are useless. They just need to be the floor, not the ceiling.
The signals that survive in 2026 are the ones the attacker can’t cheaply spoof at scale. Roughly in order of cost-to-attacker:
A Playwright-driven Chromium has a different TLS ClientHello than a real Chrome from a real desktop. Same advertised version, different cipher suite ordering, different extension list. JA4 captures this in a hashable form.
What to do: log the JA4 of every login request, cluster, and look for clusters that account for an outsized share of failed logins. We covered this in detail in JA4 Fingerprinting Against AI Scrapers: the same playbook applies to login endpoints.
HTTP/2 settings frames, header order, and pseudo-header order vary by client library. The Go net/http HTTP/2 implementation, Python httpx, and a real Chrome are trivially distinguishable. Akamai’s Akamai-H2 fingerprint and the http2-fingerprint open-source projects formalize this.
A login request whose H2 fingerprint says “Go client” but whose User-Agent says “Chrome 124 on macOS” is automated. Full stop.
This is the highest-signal, lowest-effort detection most teams skip. You don’t need to know the attacker. You need to know the credential.
When a login attempt arrives, hash the username:password pair (with a per-tenant salt) and look it up against:
(user_hash, password_hash) pair was attempted in the last 24 hours from a different IP/JA4, it’s almost certainly a checker cycling proxies.This single check catches the bulk of low-effort campaigns and is invisible to the attacker.
Real users land on /login from a referrer, scroll, focus the email field, paste or type, blur, then submit. The whole sequence takes 4–30 seconds. A checker hits /login once for the CSRF token and POST /api/login 200ms later, sometimes from a different proxy.
Useful pre-login signals to capture from the page itself:
pointermove events fired between page load and submitEnter keydown vs. mouse click on the buttonWe dive into the keystroke side of this in the FCaptcha keystroke biometrics post. The same telemetry pipeline feeds login defense.
Real users don’t fetch /api/login directly. They go through the form. So expose a never-linked, never-rendered endpoint like /api/v1/authenticate-legacy that no human will ever hit, and treat any POST to it as automated. Same idea for hidden form fields named password_confirm that should always be empty on submit.
This is the credential-stuffing analogue of the endpoint and form honeypot patterns used elsewhere on the site.
Operators rely on response differences to classify attempts. If your 200 OK + "Invalid credentials", 200 OK + redirect to MFA, and 200 OK + session cookie look meaningfully different in size, headers, or timing, you’re feeding the KEYCHECK block.
Make every login response: success, fail, MFA-required, locked, throttled: return the same status code, the same body length (within a small jitter window), and the same baseline timing. Encode the actual outcome in a body the client parses after a server-set cookie or a signed token. The attacker’s checker sees noise; the legitimate browser sees a normal flow.
This one defense alone makes config development dramatically more expensive.
A defense stack that holds up against current tooling looks roughly like this:
(user, password) against breach corpora and a short-term seen-cache. Force a step-up on hits.Notice what isn’t on the list: a giant CAPTCHA wall, a bigger IP blocklist, or aggressive rate limits that break NAT’d users. Those are the defenses attackers have already priced in.
Two trends to watch over the next 12 months:
LLM-driven checkers. Instead of hand-written LoliCode configs, operators are starting to use LLM agents that can navigate a login flow, parse the response semantically, and self-heal when the form changes. This collapses the time between a target site shipping a defense and a working bypass. TLS and HTTP/2 fingerprinting hold up here because the LLM still has to make HTTP calls through some runtime, and that runtime has a fingerprint.
Session token harvesting. As MFA adoption rises, the economic value shifts from raw valid:hits to active session cookies. AitM phishing kits like Evilginx and Tycoon already monetize this. Your login defense doesn’t stop the phish, but binding sessions to JA4 + device fingerprint + IP-ASN tuple makes a stolen cookie expire the moment it leaves the victim’s browser.
Credential stuffing is no longer a brute-force problem. It’s a content-delivery problem dressed up as authentication: leaked credentials, residential bandwidth, and stealth automation, delivered to your POST /login at a price the attacker has already optimized.
The good news is that the same asymmetries that make the attack cheap: generic tooling, shared infrastructure, replayed credentials. Also make it detectable, if you instrument the right layer. TLS fingerprints, pre-form telemetry, breach-corpus lookups, decoy endpoints, and response symmetry are the pieces of a defense that doesn’t fall over the first time the attacker swaps proxies.
WebDecoy ships these signals (JA4, behavioral telemetry, endpoint decoys, and combo intelligence) as a single layer in front of your login endpoint, so you’re not stitching them together yourself.
If you want to see what hits your /login today, start a free trial and point WebDecoy at it for 14 days. The first surprise is almost always the volume.
OOPSpam filters form spam but can't detect AI scrapers or headless browsers. Learn why behavioral analysis beats spam filters.
securityWebDecoy now tracks bots as persistent actors and pushes a JA4 rule to your AWS WAF or Cloudflare, blocking rotating scrapers across every IP they use.
securityAI-generated form spam is harder to catch than the old kind. An honest technical breakdown of what works, what fails, and where the arms race is going.
securityLike this post? Share it with your friends!
Get a personalized demo from our team.