One session sent multiple distinct User-Agent strings (mid-session UA rotation)

net.ua_rotation_within_session  ·  convicts

networklayer
coherencecategory
1evaders caught

What it catches

Within-session UA coherence — the third within-session-invariant rotation tell, the temporal completion of net.ja4_unstable_within_session (TLS engine) and net.ip_rotation_within_session (network origin). A real client presents ONE fixed User-Agent for a session's lifetime: the UA string is pinned per browser build, and changing it requires a process restart (a new session). The edge emits the raw http_user_agent on EVERY forwarded request; the detector accumulates the distinct UAs under a ks_sid in ingest (a running ua_seen set survives the latest-per-kind merge collapse) and derives a sticky network.ua_rotation when >=2 distinct UAs appear. CLOSES THE SAME-ENGINE GAP: a UA-rotating scraper that cycles Chrome→Firefox→Safari is already caught per-request by net.tls_vs_ua_browser (UA disagrees with the invariant JA4 engine), but one that rotates WITHIN an engine family — cycling Chrome BUILD strings (different versions, or Chrome-Win/Chrome-Mac/Chrome-Linux), the realistic 'look like many users on one proxy' tactic to dodge UA-keyed rate limits/blocklists — keeps JA4/h2/Sec-CH-UA coherent and slips past every cross-layer UA rule; this within-session count catches it regardless of engine because it compares the UA to ITSELF over time. Threshold >=2 (stricter than ip_rotation's 3): unlike IPs there is NO legitimate mid-session UA change — no UA analog of a CGNAT pool or wifi<->cellular handoff — so a single distinct extra UA is already impossible for a real browser. FP-safe by construction: browserforge calibration carries no network/UA layer, so promotion cannot raise its legit-browser flag rate, and the negative is grounded across the whole fleet (every single-UA evader/real-browser capture sends one http_user_agent -> no fire). GROUNDED LIVE 2026-06-20: minted one ks_sid via the edge, then replayed the SAME cookie through the edge reverse-proxy with three distinct same-engine Chrome UAs (Chrome 124/125/126 on Windows — identical engine/h2/OS) -> the detector accumulated 3 distinct http_user_agent and tripped this rule (label bot) with NO cross-layer UA rule firing (the same-engine rotation is invisible to tls_vs_ua_browser/h2_vs_ua_browser/ch_platform), proving the gap is real and this is the only tell that closes it; every single-UA session in the fleet does NOT fire it. Residual caveat (documented, same external-proxy class as net.ja4_unstable's): a UA-rewriting forward proxy fronting a real browser could present >1 UA, but the lab edge is the first hop (sees the client directly), so that residual is an external-proxy edge case, not an in-fleet FP.

Signals it reads

network.ua_rotation

How it fires

present

Evaders it caught 1

Bypassed by 10

Frontier evaders that reach the detector uncaught (scored only suspicious, defeating every convicting tell) — this check is not one that stops them. The red-team frontier this detection still has to convict.

← all detections