All articles Advanced signals

Detecting Selenium-Wire Proxy Interception

Selenium-Wire wraps Selenium with a local intercepting proxy so scripts can read and rewrite every request and response, which is why it shows up in scraping and credential-testing stacks that need per-request proxy control. That interception is powerful, and it is also the thing that gives the setup away, because a man-in-the-middle layer cannot re-emit traffic byte-for-byte the way the original browser engine would.

How Selenium-Wire works

Under the hood, Selenium-Wire launches a local proxy and points the browser at it. The browser’s requests hit the proxy, which terminates TLS with its own certificate, hands the plaintext to the Python layer for inspection or modification, then re-originates the request upstream, often through a rotating external proxy. Everything the script sees and edits passes through that re-termination step.

The TLS fingerprint shift

A browser’s TLS ClientHello is a rich fingerprint: cipher suite order, extensions, supported groups, and ALPN values that are characteristic of the specific engine and version. When Selenium-Wire’s proxy re-terminates and re-originates the connection, the handshake that reaches the server is generated by the proxy’s TLS stack, not Chrome’s. The result is a JA4 or JA3 fingerprint that does not match the browser the User-Agent claims. A session announcing Chrome 130 while presenting a Python-library TLS signature is contradicting itself at the transport layer, and no page-level patch can fix that because it happens below the browser.

Header order and semantics

HTTP/2 header ordering is engine-specific. Chrome emits pseudo-headers and regular headers in a consistent, well-known sequence. A proxy that parses and re-serializes requests frequently reorders headers, normalizes casing, or drops and re-adds fields, producing an order that does not match any real browser. Injected custom headers, duplicated Accept values, and inconsistent Accept-Encoding are common tells from scripts that edit requests in flight.

Correlating transport with the browser

The strongest detection compares layers that should agree. A genuine visitor’s TLS fingerprint, HTTP/2 header order, User-Agent, and JavaScript-reported environment all describe the same browser. Selenium-Wire breaks that agreement because the transport is generated by a proxy while the JavaScript environment is generated by the real Chromium. Our network and TLS signals are built to catch exactly this kind of cross-layer contradiction, where the wire tells one story and the DOM tells another.

Behavioral and identity signals

Transport artifacts are decisive, but they rarely travel alone:

  • Scripted input. Selenium’s ActionChains produce evenly timed, geometrically clean pointer and keystroke events.
  • Per-request proxy rotation. Selenium-Wire often swaps the upstream proxy per request, so a single session’s requests exit from IPs spread across ASNs and geographies in a way real users never produce.
  • Cross-session identity. The same Selenium-Wire image and profile pool power many workers. A stable visitorId collapses the “unique” sessions to a few real identities.

Why operators accept the risk

If Selenium-Wire is so detectable at the transport layer, why do scrapers keep using it? Because the capability it provides, reading and rewriting every request and response in Python, is genuinely hard to replace. Teams need it to inject authentication headers, capture JSON response bodies without re-parsing rendered HTML, and assign a different upstream proxy to each request for maximum rotation. Those are real engineering wins, and the operators betting on them often assume defenses stop at the JavaScript layer. They do not. A detector that fingerprints the wire sees the intercepting proxy’s TLS stack regardless of how clean the DOM looks, which means the very feature that makes Selenium-Wire useful, sitting in the middle of the connection, is the feature that exposes it. The tool cannot both rewrite traffic freely and reproduce Chrome’s exact handshake, so operators are choosing capability over stealth whether they realize it or not.

A detection recipe

To flag Selenium-Wire interception:

  1. Fingerprint TLS with JA4 and compare it to the browser the UA claims.
  2. Verify HTTP/2 header order and casing against the expected engine profile.
  3. Watch for per-request exit-IP scatter within one session.
  4. Score behavioral flatness from scripted input.
  5. Correlate the visitorId across sessions and against reputation data.
  6. Emit reason codes so the transport-versus-DOM contradiction is visible to reviewers.

Why this is hard to evade

To hide Selenium-Wire, an operator would need the intercepting proxy to reproduce Chrome’s exact TLS handshake and HTTP/2 framing while still parsing and rewriting traffic, which defeats much of the point of a general-purpose MITM proxy. The interception and the disguise pull against each other, and that tension is the detector’s advantage.

Selenium-Wire buys scripts total control over requests at the cost of a transport signature that no longer matches the browser. Once you score the wire and the DOM together, that mismatch is one of the cleaner bot tells there is.

See how cross-layer signals score an intercepted session in the playground, free to start, or compare tiers on pricing.

Try it free

Prynt is device intelligence with a free tier — visitor IDs, bot & fraud Smart Signals, and behavioral biometrics, powered by a cross-site network. Start free.

Keep reading