Defeat IP Rotation: Block Bots by JA4 at the WAF
WebDecoy 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.
WebDecoy Team
WebDecoy Security Team
A few years ago, form spam was easy to spot. You looked for misspellings, broken English, the same boilerplate paragraph submitted ten thousand times from a Russian VPS, and a stuffed honeypot field. Anti-spam was a solved problem at the level most sites needed.
That stopped being true sometime in 2023, and by 2026 the landscape on a public contact form, sign-up flow, or review submission page looks meaningfully different. The submissions are grammatically clean. They reference your product by name. They cite the right page on your site. They sign off with a plausible name and a working-looking email. And they show up at a steady rate from residential IPs, on real Chrome builds, often through a Browser-as-a-Service.
This post is a candid look at what we’ve learned trying to detect this stuff. We’ll walk through what doesn’t work, what does, and where the bottom keeps falling out. If you’re hoping for a “one weird trick” ending, this isn’t it.
The term gets sloppy fast, so we’ll be specific. Three distinct populations show up at a typical form endpoint, and they need different defenses.
A defense that catches category 2 may have nothing to say about category 3. A signal that’s perfect against category 3 may flag the legitimate humans in category 1. Conflating these is how you end up with a content classifier as your only defense and a queue full of false positives.
Before we get to the parts that do hold up, it’s worth being specific about the dead ends, because they keep getting sold as solutions.
The first reflex is to send the body to a classifier. GPTZero, OpenAI’s deprecated detector, the various open weights detectors. We tried all of them on a labeled corpus of real contact form submissions plus three months of confirmed AI submissions.
The results were grim. On the AI side, instruction-tuned outputs from GPT-4-class and Claude-class models score in the same distribution as the legitimate human pile for almost every detector. Add a single prompt instruction like “vary your sentence length and use one minor typo” and the remaining gap closes. On the human side, non-native English speakers and anyone using Grammarly aggressively get classified as AI at rates that make the detector unusable as a hard gate.
Text classifiers have a place as a scoring input feeding a layered model. They do not have a place as a binary spam flag.
Same family, same problem. Perplexity-based detection assumes AI text is too statistically clean. That assumption was reasonable in 2022 and is not reasonable now. Modern models produce output with perplexity profiles indistinguishable from competent human writing, and any small temperature or sampling tweak shifts the curve.
We still log perplexity as a feature. We don’t gate on it.
Google and OpenAI have both published work on cryptographic watermarks embedded in generated text. The idea is sound. The problem is operational. Watermarks only exist for models whose providers chose to embed them, only persist if the operator doesn’t paraphrase, and only help defenders who can run the matching detector. None of the open weights models that operators actually use carry usable watermarks, and a single rewriting pass through a different model strips whatever signal might have been there.
Watermarks are a real research direction. They are not a 2026 defense.
reCAPTCHA v2 image puzzles are solved by 2Captcha and CapSolver for around two dollars per thousand. reCAPTCHA v3 score-based gating fires false positives at ordinary humans on residential VPNs and lets through any agent that can drive a real Chrome with realistic mouse movement. hCaptcha is in roughly the same place.
For the LLM-agent population in particular, captcha is mostly self-deception. The agent sees the captcha, calls a vision model, solves it, and continues. We covered the vision-side of this problem in Detecting Vision-Based AI Agents.
What survives is not exotic. It’s a set of cheap signals that, combined, raise attacker cost faster than any one of them does alone. Roughly in order of how often each one carries the day for us:
This is the single highest-yield signal we have, and it’s almost free to ship.
Real users type into form fields. They produce a long stream of keydown, keypress, input, and occasional paste events. Inter-keystroke intervals look like a human nervous system: variable, with a heavy tail, plus the occasional pause to think.
LLM-assisted humans (category 1) almost always paste. The textarea gets a single paste event followed by zero input events that look like typing. That’s not inherently suspicious, but combined with other signals it’s strong.
LLM-driven agents (category 3) are worse for the attacker. Most browser-driving frameworks set field values by assigning to element.value directly or dispatching synthetic input events without the underlying keydown/keypress sequence. Detecting “value changed but no real input events fired” catches a surprising fraction of agent traffic without any model, just a few lines of JS.
The instrumentation is small:
const field = document.querySelector('textarea[name="message"]')
let realInputEvents = 0
let pasteEvents = 0
let valueAtLoad = field.value
field.addEventListener('keydown', () => realInputEvents++)
field.addEventListener('paste', () => pasteEvents++)
// On submit, sample these into a signed token sent with the POST.
form.addEventListener('submit', () => {
const finalLength = field.value.length
const typedRoughly = realInputEvents
const pastedAtLeastOnce = pasteEvents > 0
const grewWithoutTyping = (finalLength - valueAtLoad.length) > typedRoughly * 2
// grewWithoutTyping is the agent signal.
})The trick is that the score has to be opaque to the attacker. You don’t want a clean Reason: scripted_value_set field in the response. We get into that under “response symmetry” below.
Real humans land on a form page, scroll a bit, focus the first field, fill in order, sometimes go back to fix one, blur, and submit. The whole sequence on a mid-length contact form is in the 15 to 90 second range.
Agents are bimodal. Cheap scripts post in 200ms. Smart agents wait, but they wait a uniform amount because someone parameterized delay = 30s in the workflow. Real human form-fill times are messy and bursty; agent times are too clean.
Useful raw signals:
pointermove near the buttonNone of these flag in isolation. All of them feed a per-session score.
Every script that POSTs to your form has a TLS ClientHello and an HTTP/2 settings frame. Real Chrome on real macOS has a JA4 fingerprint that’s stable and well-known. Python httpx, Go net/http, Node undici, and a Playwright-driven Chromium each have their own.
A POST that arrives with a User-Agent: Chrome 124 on Windows and a JA4 that says “Go HTTP client” is automated. That’s a hard signal, not a probabilistic one.
This catches most of category 2 (scripted senders) at the network layer before the form handler even runs. We’ve written about the JA4 side of this in detail in JA4 Fingerprinting Against AI Scrapers, and the same playbook applies at form endpoints.
For the agent population, you want to know whether the page is being driven by an automation framework. Standard tells include:
navigator.webdriver === truechrome.runtime on a UA that claims ChromeStealth patches close most of the easy ones. The arms race here is long and we maintain dedicated coverage of it in Headless Browser Detection and Browser-as-a-Service Detection. The short version: this is a useful layer, not a sufficient one.
Classic honeypots use display: none or visibility: hidden or off-screen positioning to hide a field from human users. A bot that parses raw HTML and submits everything fills the field. A human leaves it empty.
This still works against most scripted submitters. It does not work against vision-based agents that read the rendered page, because the agent literally cannot see the field. It also does not work against careful headless agents that filter display: none inputs.
What still works in 2026 is honeypots that look like normal fields but encode something about the DOM order or attribute pattern that no human would interact with. A field named email_confirm placed after the submit button. A <select> with an autofill-friendly name and a hidden default option. A field rendered into a Shadow DOM that mainstream automation libraries don’t traverse cleanly.
We go deeper into the taxonomy in Honeypot Traps for Forms, Buttons, and Endpoints.
This one is borrowed from the credential stuffing playbook and applies just as well to forms. The attacker tunes against your responses. If your form returns “Thanks, we got it” on success and “Looks like spam, sorry” on rejection, you have just told the attacker exactly what to optimize against.
Make every form submission, valid or rejected, return the same status code, the same body length within a small jitter window, and the same baseline timing. Surface the actual outcome to the legitimate user via a server-set cookie or a signed token that the page reads after submit. The legitimate browser sees “Thanks.” The attacker’s checker sees noise.
This single change makes building reliable AI-form-spam tooling against your site dramatically more annoying.
Once a submission has cleared the cheap signals, content scoring earns its place, but only as the last layer and only with the right framing. We don’t ask “is this AI-written” because we showed earlier that the answer is unreliable. We ask:
These are old-school spam features, and they still pull weight. The LLM changes the surface, not the underlying intent.
The reason this problem is hard, and why anyone telling you they’ve “solved AI form spam” is overselling, is that every signal we list above has an attacker-side response that costs them less than the defense costs you to maintain.
dispatchEvent for keydown/keypress/input in sequence. The latest versions of Browserbase already do this by default.The goal of detection is not to “win.” It’s to keep raising the cost of high-volume automated abuse faster than the attacker can drive it down, and to make sure the floor of cheap, lazy attacks falls cleanly into a deny bucket without ever reaching a human moderator. Every signal we ship pushes the attacker toward more expensive infrastructure (real Chromium, real residential bandwidth, careful per-target prompt engineering), and the economics start to fall apart somewhere in the middle of that ladder for anyone who isn’t running a serious operation.
That ladder is moving up over time, and it will keep moving up. We don’t think there’s a stable equilibrium here. We think there’s a continuous arms race, and the right posture for a defender is to instrument enough layers that you keep collecting data on what the next round of attackers tries.
Concretely, WebDecoy ships a JS snippet on protected forms that captures:
isTrusted-aware)On the server side, we cross-check that token against:
Every flagged submission ends up in a customer-visible queue with the specific signals that fired, so you can audit the false-positive rate against real traffic. We do not gate on text classifiers, perplexity scores, or watermark detection, for the reasons laid out above.
If you’re running a contact form, sign-up flow, review submission, or any other public form that’s started catching grammatically clean garbage, this is the layer we recommend you add before you reach for a captcha. Start a free trial and point it at your form for 14 days. The interesting part is usually not the volume. It’s the shape of what makes it through.
Not reliably enough to gate on. Modern instruction-tuned models produce text that scores like human writing on every public detector we tested, and operators can lower perplexity further with simple prompt tweaks. Text-only classifiers belong in a scoring stack, not as a single gate.
Paste detection. Most LLM-driven submissions arrive as a single paste event into the textarea or as a programmatic value set with no input events at all. A few lines of JS to track input vs paste vs scripted assignment catches a large share of low-effort campaigns.
Against headless HTTP scripts yes. Against vision-driven agents like Computer Use or Operator, classic CSS-hidden honeypots are increasingly visible because the agent reads the rendered page: honeypots that target the DOM tree rather than the visual layout still work, and we cover that distinction in our honeypot guide.
No, and anyone selling one is overpromising. The defense that holds up is layered: behavioral telemetry, paste and automation signals, response symmetry, and post-submit content scoring as a tiebreaker. Each layer is cheap. The combination is what raises attacker cost.
WebDecoy 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.
securityA technical deep dive into credential stuffing tooling, attack anatomy, and the detection signals that actually work against modern ATO operators.
securityA technical audit of browser fingerprinting in 2026. Which techniques survive Privacy Sandbox, anti-fingerprinting browsers, and stealth automation.
securityLike this post? Share it with your friends!
Get a personalized demo from our team.