fix: unblock route handlers, frame lookup, and worker/persona integrity - #185
Open
DemonMartin wants to merge 1 commit into
Open
fix: unblock route handlers, frame lookup, and worker/persona integrity#185DemonMartin wants to merge 1 commit into
DemonMartin wants to merge 1 commit into
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 71fbb0f9ff
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Author
|
Fyi, I used it together with https://github.com/clearcotelabs/clearcote-browser to bypass turnstile |
Six defects found while driving a hardened Chromium build through rustwright.
Each was reproduced first, then fixed:
evaluate: `looks_like_function` searched the whole source for `=>` and accepted
the expression when the text before it started with `(`. Every IIFE starts with
`(`, so one containing an arrow anywhere in its body was read as a function
literal, wrapped as `const __rw_fn = (<iife>)` and called again -- the IIFE had
already produced its value, so the call failed with "__rw_fn is not a function".
The head of the expression is now parsed instead: the arrow must bind the
leading parameter list, and the scan skips string literals, template literals,
comments and regular expressions so `(a = ")") => a` and `(p = /[)]/) => p`
still resolve. Keyword prefixes no longer match, so `functionality()` is not a
function literal.
frame tree: `refresh_page_frame_tree` gave every session the caller's entire
remaining deadline for what the function itself treats as optional -- a session
that does not answer is skipped and the cached tree returned anyway. A renderer
does not answer `Page.getFrameTree` while a Fetch interception is paused on its
document request, which is exactly the state a route handler runs in, so
reading `Request.frame` there burned one full deadline per session and stalled
the navigation that request belonged to. With the timeout disabled, where
`command_timeout` maps 0 to 24 hours, it never returned. The refresh is now
capped independently of the caller's budget, and the Python shim no longer
re-issues the same command over a raw session at `CDPSession.send`'s fixed 30s
once per frame walked.
workers: dedicated workers were given their identity by replacing the page's
`Worker` constructor with one that builds a blob wrapping `importScripts(url)`.
That moves every worker off its own script URL, changing its `location` and
origin, and breaks pages whose workers rely on either. Dedicated workers are
now auto-attached like service workers already were, and the identity script is
installed over the worker's own session while it is paused, so the worker keeps
its real URL. Verified: a worker's `navigator.userAgent`, `platform` and
`language` still match the page, and `location.href` is now the script's own URL
rather than a `blob:`.
stealth locale: the user-agent override pinned `acceptLanguage` to `en-US,en`,
overriding `--accept-lang`/`--lang`. A browser launched for one region reported
that region's timezone next to an `en-US` locale. Omitting the field leaves the
browser's own value, which keeps the documented default for a stock launch
(verified `en-US,en;q=0.9`) while honouring an explicit one.
stealth webdriver: the init script removed `navigator.webdriver` from browsers
already reporting `false` -- the value every real Chrome reports -- trading a
correct answer for a missing property. It now guards only that block, so the
rest of the script still runs on an ordinary launch, where the default args
already clear the automation flag.
frame enumeration: child frames came from a `querySelectorAll('iframe,frame')`
result, with the protocol's children used only to enrich it by index. A frame
inside a shadow root is invisible to that query while being an ordinary child of
the tree, so it was dropped entirely, and pairing the two lists by position gave
a light-DOM frame the identity of a shadow frame ahead of it. The protocol now
decides which frames exist and the two are correlated by identity; the DOM query
still orders and names them. This adds shadow DOM support to `page.frames` and
`frame.child_frames`.
Adds thirteen Rust unit tests covering the expression classifier, the refresh
budget, and the scope of the webdriver guard.
DemonMartin
force-pushed
the
fix/route-handler-frames-and-stealth-locale
branch
from
July 28, 2026 13:00
71fbb0f to
49699fe
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Six defects found while driving a hardened, anti-detect Chromium build through rustwright. Each was reproduced against a real browser first, then fixed.
Which block actually causes the rejection
The stealth init script has four independent blocks after the webdriver handling. Each was disabled in isolation against a live challenge:
Workerconstructor rewritewindow.chromeobjectOnly block 4 matters. The other three, including the synthetic
window.chrome, are not implicated: a surface diff against Playwright driving the same browser showedwindow.chromeidentical between the two.1. Dedicated workers are moved off their own script URL
The page's
Workerconstructor was replaced with one that builds a Blob wrapping the real script:Every worker the page creates therefore runs from a
blob:URL instead of its own. Itslocationand origin change,worker-srcCSP no longer matches, and any worker resolving a URL against its own location resolves against the blob. This is not only detectable — it changes what the worker is.The mechanism existed because
Target.setAutoAttachexplicitly excludedworker, so a dedicated worker had no session to install identity over. It is now included, and identity is installed over the worker's own session while it is still paused — exactly how service workers were already handled — so the rewrite is unnecessary and is removed.Verified directly, driving the same browser with
--fingerprint-platform=windows --lang=de-DE:The identity guarantees the wrapper provided are unchanged; the worker now keeps its real URL.
2.
page.evaluate()rejects an IIFE containing an arrow functionlooks_like_functionsearched the entire source for the first=>and accepted the expression when the text before it started with(. Every IIFE starts with(, so one whose body contained an arrow was classified as a function literal, wrapped asconst __rw_fn = (<iife>); return __rw_fn()and called a second time — on the value the IIFE had already returned:The head of the expression is now parsed rather than the whole body scanned: the arrow must bind the leading parameter list. Finding that list means matching the opening parenthesis, so the scan skips string literals, template literals, comments, and regular expressions —
(a = ")") => a,(a /* ) */) => aand(pattern = /[)]/) => patternall resolve correctly. Keyword matching is whole-word, sofunctionality()is no longer read as a function literal.3.
Request.frameinside a route handler stalls the navigationrefresh_page_frame_treegave each session the caller's entire remaining deadline, for a refresh the function itself treats as optional — a session that does not answer is skipped and the cached tree returned anyway.A renderer does not answer
Page.getFrameTreewhile aFetchinterception is paused on its document request, which is precisely the state a route handler runs in. ReadingRequest.framethere burned one full deadline per session walked, and the navigation whose request was paused could not proceed meanwhile. Measured with a 4 s default timeout,page.framestook 38 s, andpage.gotothen failed withTimeout ... exceeded. With the timeout disabled it never returned, becausecommand_timeoutmaps0to 24 hours.The refresh is now bounded independently of the caller's budget. Separately, the Python shim re-issued the same command over a raw
CDPSessionwhenever the core's tree came back empty — an empty tree is a legitimate answer for a document that has not committed one yet, and re-asking costCDPSession.send's fixed 30 s once per frame walked. The core owns this tree, so the shim no longer duplicates the round trip.4. The stealth override pins
Accept-Language, overriding the configured localeinstall_stealth_defaultshardcoded"acceptLanguage": "en-US,en"into the user-agent override applied to every session, taking precedence over--accept-langand--lang. A browser launched for one region reported that region's timezone next to anen-USlocale.Omitting the field leaves whatever the browser was configured with. Verified both directions:
Accept-Languageheadernavigator.languagesen-US,en;q=0.9['en-US', 'en']--accept-lang=de-DE,dede-DE,de;q=0.9['de-DE', 'de']The first row is what
test_default_request_headers_do_not_expose_headless_chromeasserts, so the documented default for a stock launch is unchanged.5.
navigator.webdriveris removed from browsers that reportfalseThe init script deleted
Navigator.prototype.webdriverunconditionally. On a stock Chromium advertising automation that is the intent; on a browser already reportingfalse— what every real Chrome reports — it traded a correct answer for a missing property.It now guards only that block, so everything after it still runs. That distinction matters: the default launch args include
--disable-blink-features=AutomationControlled, sonavigator.webdriver === falseis the common path, and an early return would have disabled the whole init script for ordinary users. A unit test now asserts the guard closes before the chrome-object setup and that the script contains no earlyreturn.This is a correctness fix, not the fix for the rejection.
6. Frames inside a shadow root are missing from
page.framesChild frames came from a
document.querySelectorAll('iframe,frame')result, with the protocol's children used only to enrich that list by position. Two consequences: a frame mounted inside a shadow root is invisible to the query while being an ordinary child of the frame tree, so it was dropped entirely; and when the lists disagreed, a light-DOM frame was handed the identity of a shadow frame that preceded it.The protocol now decides which frames exist, and the two are correlated by frame identity rather than by index. The DOM query still supplies ordering and names for the frames it can see. This adds shadow DOM support to
page.framesandframe.child_frames, so widgets that mount their cross-origin iframe that way are reachable.Tests
Thirteen Rust unit tests: the expression classifier (IIFEs, parenthesised expressions containing arrows, genuine arrow and function literals, keyword-prefixed identifiers, literals/comments/regexes inside the parameter list, nested parameter lists), the refresh budget including the disabled-timeout case, and the scope of the webdriver guard.
cargo check --locked,cargo fmt --checkandcargo test --libpass (107 tests).pytest tests/test_antibot_benchmarks.pypasses (15 tests).The browser-backed pytest files could not run on this machine: the bundled Chromium 151 aborts with
SIGTRAPbefore the CDP endpoint opens, when launched directly from a shell and independently of rustwright, with no missing shared libraries. PerAGENTS.mdthat verification belongs on a testbox rather than a developer host, so those suites still need a CI run — in particular the worker-identity tests, which this PR changes the mechanism behind. The equivalent assertions were checked by hand against a real browser, as shown above.End-to-end verification
Against a live challenge through a proxy, with a route handler reading
Request.frameon every request:page.gotofailed withPage.goto: Timeout 45000ms exceededThree consecutive runs with the full init script active — no blocks skipped — completed successfully. The reproductions are not included here: they need a proxy and a third-party browser build, so they cannot run in CI.