RQ4

RQ4-S: detecting cookie-reuse handoff in session-level header analysis

June 15, 2026

The cookie-reuse handoff attack defeats per-request bot detection without sophisticated tooling. A real browser visits the protected site, solves whatever JS challenge the WAF presents, receives a session cookie that says "this client is trustworthy," and then exports that cookie to a bot. The bot makes its subsequent requests carrying the legitimate cookie, and the per-request defenses see what looks like a returning trusted user. Each individual request passes inspection; the attack is invisible at the per-request layer.

RQ4-S (the session-level extension of the RQ4 specification at rq4.dev, Section 6) catches this attack by correlating per-request RQ4 vectors across requests sharing the same session cookie. The browser phase of the session produces RQ4 vectors consisting only of valid (v) or indeterminate (-) characters; the bot phase produces vectors containing impossible (x) characters because the bot can't replicate the browser's contextual header dispatch perfectly. The transition from clean to impossible mid-session is the signal.

This post walks through the attack, the detection state machine, what it catches, and what it doesn't.

What this looks like in practice

Common scenario: a mid-size B2B SaaS, ~$50K MRR, runs Cloudflare's free JS challenge on the signup form. The challenge has stopped about 70% of bot signup volume since it was enabled, a significant win. But signup-to-paid conversion is still suspiciously low: 30% of signups never log in a second time, use temp-mail addresses, and consume the free-trial credit budget in a single batch before disappearing.

Investigation: write a small script that logs cf_clearance cookie values + per-request IP + RQ4 vector for every signup attempt over 24 hours. Run it.

The pattern: at 11:47am, a single request comes in from a residential IP, solves the Turnstile challenge in a real Chromium (the request bears all the JS-evaluated cookies, the JS-execution timestamp is plausible). Receives cf_clearance cookie. Browser session ends.

At 11:52am, a burst of 1,247 requests arrives. Different IPs (residential pool, 800+ unique IPs). Different User-Agent strings (the bot rotates UAs across requests). One thing in common: every single request carries the exact same cf_clearance cookie value the legitimate browser received 5 minutes ago.

Whoever ran the challenge once is now selling/replaying the cookie at scale. Each individual request looks legitimate to Cloudflare: valid cookie, in-window, IP from a residential pool, header sets that don't fail Cloudflare's per-request checks.

The fix is RQ4-S: track header consistency across the requests sharing each cookie. The original browser produced clean RQ4 vectors (vvvv); the replaying bots produce xxvx because they reuse navigation-style headers on what should be post-navigation requests. The transition from clean to impossible mid-session is detected on request #2 of each handoff batch.

Deployed the RQ4-S check in a Cloudflare Worker in front of the signup endpoint. The 11:52am burst the next morning never gets past the second request, every replay attempt fires rq4s_transition and gets a 403. Real users (carrying their own freshly-issued cookies) pass through.

The temp-mail signup rate drops from ~30% to ~4% over the following week. The remaining ~4% is presumed to be actual humans using temp-mail for their own reasons (anonymity, throwaway accounts, etc.), that's a different problem and not one bot-detection alone can solve.

The attack

The cookie-reuse handoff has three steps:

  1. Browser phase. An attacker visits the target site in a real browser (Chrome, Safari, Firefox). The site's WAF (Cloudflare's JS challenge, Imperva's reese84, Akamai's sensor-data challenge, F5 Shape's signed messages) serves a JavaScript challenge. The browser executes the JS, produces a proof-of-execution value, and is granted a session cookie. The cookie is valid for 30 minutes to 24 hours depending on the WAF.

  2. Cookie export. The attacker extracts the cookie. Browser extensions, manual copy-paste from devtools, or a headless Chrome script that runs the challenge and exports the result. Selling these cookies is a real market, pre-solved session cookies for protected-site.com go for $0.10-$2 each on scraper forums.

  3. Bot replay phase. The attacker's bot, typically curl_cffi or a custom HTTP client, makes requests to the protected site carrying the exported cookie in the Cookie header. The WAF sees a session it issued, in the cookie's TTL, and lets the request through. The bot scrapes pricing, harvests data, runs credential-stuffing, posts spam, whatever the original objective was. Each individual request looks legitimate.

The attack works because per-request detection has nothing to compare against. The first request with a bot-looking pattern is treated as a new visitor (issue new challenge, request the JS execution). With the cookie, that first inspection is skipped, the request is "returning user," verified.

What RQ4-S looks for

The RQ4 fingerprint captures four dimensions of header consistency per request: Mode, Upgrade, Identity, Transfer. Each dimension produces v (valid combination per the Fetch spec), x (impossible combination, browsers cannot produce this), or - (indeterminate from available data). A real browser session produces vectors like vvvv, vv-v, or occasionally --v-. A bot that reuses navigation-style headers on cors-style requests produces vectors like xxvx (impossible on three of four dimensions).

The cookie-reuse handoff produces a session signature that looks like:

Request 1 (real browser, GET /):   rq4 = vvvv   → session is clean
Request 2 (real browser, POST /api): rq4 = vvvv → session is clean
[cookie exported to bot]
Request 3 (bot, POST /api):        rq4 = xxvx   → TRANSITION detected
Request 4 (bot, POST /api):        rq4 = xxvx   → continued bot activity

A session that produced only clean vectors for two requests cannot legitimately produce xxvx on its third. Real browsers don't change their header-dispatch logic mid-session. The transition is the cookie handoff.

The state machine

Per the open RQ4 v2.0 specification, Section 6:

State:
  clean:       bool   (true = only v/- vectors observed)
  count:       int    (total requests in this session)
  flagged_at:  int    (request number when clean → flagged, if any)

For each request with cookie C and RQ4 vector V:
  state = get_or_init(C)
  state.count += 1
  has_impossible = "x" in V

  if has_impossible and state.clean and state.count > 1:
    signal = "rq4s_transition"   # the smoking gun
    state.clean = false
    state.flagged_at = state.count
  elif not state.clean:
    signal = "rq4s_continued"    # continued bot activity on flagged session
  else:
    signal = none                # request is consistent with session state

The count > 1 requirement on the transition rule is intentional: a bot's very first request on a fresh session cannot be a "transition" because there's no prior clean history to transition from. That case is handled by per-request RQ4 analysis, the request still produces an x-bearing vector that flags at the per-request layer. RQ4-S specifically catches the handoff pattern where a session was clean and now isn't.

A reference simulator for this state machine verified seven canonical scenarios: pure browser sessions produce no signal, clean-then-bot transitions fire, continued bot activity fires, bot-from-request-one doesn't fire (no prior clean history), bot-rotating-cookies produces no session signal (each cookie is a fresh session of count=1), and the documented evasion ceiling (bot maintaining vvvv throughout) passes through. Implementing the algorithm above against these test cases is a one-afternoon project; the spec is small.

What this catches

The cookie-reuse handoff is one of the most common bypass patterns for JS challenges. Empirically observed against:

In each case, the WAF assumes that whatever solved the JS challenge will continue to make subsequent requests. RQ4-S doesn't make that assumption, it correlates header consistency across requests directly, regardless of what the WAF blessed.

What this misses

A bot that maintains "browser-like" headers throughout the session passes RQ4-S. The detection requires an actual x in the RQ4 vector to fire. If the attacker invests in proper contextual headers, Sec-Fetch-Mode: cors on fetch-API POSTs, Sec-Fetch-Mode: navigate only on actual top-level GETs, proper Accept and Accept-Encoding per request type, there's no x to detect, no transition to flag.

This is the documented evasion ceiling. It's the same ceiling that any header-only detection scheme has. Closing it requires layers that operate outside the request headers entirely:

None of these are RQ4-S. They're additional layers that catch what RQ4-S can't.

Why RQ4-S is the most cost-effective layer to ship first

Among the bot-defense layers, header-based session-level correlation has the best cost-to-coverage ratio for a small team:

The cost is real but bounded: per-session state needs to be stored somewhere with a TTL matching the longest cookie lifetime you care about (~20 minutes for most WAF challenges). For Cloudflare Workers, KV is sufficient at this scale.

Limitations

Common questions

Doesn't Cloudflare itself detect this?

Not on free or Pro plans. The JS challenge issues cf_clearance cookies; subsequent requests carrying a valid cf_clearance are trusted. Cloudflare's higher tiers (Bot Management on Business/Enterprise plans, $300+/month base, behavioral signals layered on top) may catch some handoff patterns via additional signals like client TLS fingerprint stability across the session. But for the millions of sites on free/Pro plans, the JS challenge is a one-shot gate, once passed, subsequent traffic isn't re-evaluated. The handoff attack exploits exactly that gap.

Why can't I just shorten the cookie expiration time?

You can, and it helps marginally, shortening cf_clearance from its 30-minute default to 5 minutes forces re-challenge more often. But (1) you can't change cf_clearance TTL on free/Pro Cloudflare plans (it's a CF-controlled cookie), and (2) even with a 1-minute window, the attacker can solve the challenge once in a Chromium and replay the cookie within that window to make hundreds of requests. Time-bounding cookies slows the attack but doesn't structurally prevent it. RQ4-S structurally prevents it because the detection is per-request, not per-session-window.

Does this work for non-Cloudflare challenges (Akamai, Imperva)?

Yes, the RQ4-S algorithm is challenge-vendor-independent. It correlates RQ4 vectors across requests sharing whichever session cookie the WAF is using. Plug in Akamai's _abck cookie, Imperva's reese84, F5 Shape's signed messages, the state machine is the same. The session-cookie identification step (which cookie do you track?) is the one part that varies per WAF; the rest of the logic is identical.

Is this what JA3 fingerprinting is supposed to solve?

No, different attack class. JA3 fingerprints the TLS handshake; useful for catching libraries that have distinctive TLS stacks (curl_cffi, tls-client, etc.). The cookie-reuse handoff attack uses a real browser to solve the challenge AND a real browser TLS stack on the replay (in our observed cases, the replayer often uses Playwright with stock Chrome TLS, which produces a clean JA3 hash). JA3 won't flag it. RQ4-S catches it because the bot's REQUEST DISPATCH LOGIC (which Sec-Fetch headers go on which method) differs from a real browser's, even when the TLS layer is identical.

Can I implement this without paying for an API?

Yes, the spec is open and the implementation is small (the reference simulator in our internal verification is about 50 lines of Python). The work involved: (1) extract a session ID from the request's cookie header, (2) maintain per-session state in a KV store or Redis (clean? count? flagged_at?), (3) compute the RQ4 vector for each request per the public spec, (4) apply the state machine. A weekend project for a competent engineer. The decision is whether you'd rather build and maintain it or pay for a managed implementation when one becomes available at this tier.

How long does the bot have to wait between solving the challenge and replaying to evade RQ4-S?

Longer than the session-state TTL. A typical RQ4-S implementation uses ~20 minutes of session state per cookie, long enough to cover the typical cookie lifetime, short enough to keep KV writes bounded. If the bot solves the challenge, waits 25 minutes, then replays, the session state has expired and the first replay request looks like a fresh session (count=1, no prior history to transition from). RQ4-S doesn't fire on that single request, but per-request signals still flag the curl_cffi headers if the bot uses curl_cffi for the replay. The economic effect: forcing a 20+ minute wait between solve and replay slows the attack tenfold and pushes the attacker toward either solving constantly (defeats the point of cookie sharing) or building Playwright-based replays (10x more expensive to operate at scale). Either way, the attack economics shift unfavorably for the attacker.

Related reading

Stay updated

We publish on request fingerprinting, browser bot detection, and running it in production. Drop your email for new posts.