Skip to content

fix(juggler): restore scoped closed-shadow access - #685

Open
Cloudymap1e wants to merge 1 commit into
daijro:mainfrom
Cloudymap1e:fix/privileged-scope-access
Open

fix(juggler): restore scoped closed-shadow access#685
Cloudymap1e wants to merge 1 commit into
daijro:mainfrom
Cloudymap1e:fix/privileged-scope-access

Conversation

@Cloudymap1e

Copy link
Copy Markdown

Related Issue

Closes #628

Description

The Firefox 146 Juggler ESM migration stopped honoring forceScopeAccess, so trusted automation could no longer reach the chrome-only shadowRootUnl accessor used for closed shadow roots.

This restores the historical explicit opt-in behavior without changing the default context or elevating the evaluator to the system principal. When forceScopeAccess is enabled, Juggler uses the same page-scoped expanded-principal/Xray pattern as its isolated worlds and installs one compartment-local shadowRootUnl getter. A narrowly exported callback backs that getter and returns only the native chrome-only shadow-root attribute; its temporary global binding is deleted immediately. Page scripts cannot see the accessor, hostile page expandos cannot intercept it, and no chrome helper or system-principal evaluation realm is exposed. When the setting is absent, Juggler continues to execute against the existing content-page window.

Type of Change

  • Bug fix
  • New feature
  • Documentation update
  • Other

Testing

  • Focused scope suite: 5 passed
  • Broader Playwright evaluation suite: 59 passed, 8 existing Camoufox-unsupported cases skipped
  • Enabled-mode coverage includes closed-root access, hostile page getter/expando isolation, privileged-global/constructor-escape checks, DOM and JavaScript handles, navigation, and child-frame context recreation
  • Default-mode coverage verifies that an absent setting does not expose shadowRootUnl
  • Security probe: cross-origin fetch, cross-origin XHR, and file:// fetch all remained blocked
  • node --check additions/juggler/content/FrameTree.js: passed
  • Independent final security review: passed; no concrete blocker found in the page-scoped accessor design

Fingerprint Report

build-tester/run_tests.sh ... --no-cert reported grade A with 1025/1048 checks across eight profiles, above the PR checklist's required score of 1000. The harness returned status 1 because it requires 1048/1048 for a zero exit; all 23 misses were local screen-size expectation differences on the bounded 1920x1080 validation host.

Validated artifact omni.ja SHA-256: b6ee2601d86c79481c4a734f7d995d6c211b4aeeadadd19246fa8ae8613d86e5.

Checklist

  • I have linked a related issue above
  • My changes are focused on a single logical change
  • I have added testing instructions which include the desired result
  • Service tests pass (for python library changes) -./service-tester/run_tests.sh --browser-version official/prerelease/146.0.1-alpha.25 (attach screenshot) temporarily out of service lol
  • Build test passes (for patch changes) - score 1023, required threshold 1000

@JWriter20 JWriter20 mentioned this pull request Jul 30, 2026
5 tasks
JWriter20 added a commit to JWriter20/camoufox that referenced this pull request Jul 30, 2026
…ld (daijro#628)

`forceScopeAccess` was declared in settings/properties.json and
settings/camoucfg.jvv, validated on the way in, and then read by nothing --
grep found no consumer anywhere in additions/juggler/. So
`element.shadowRootUnl`, which patches/shadow-root-bypass.patch adds to
Element.webidl gated on Func="Document::IsCallerChromeOrAddon", was `undefined`
from page.evaluate() no matter what the flag was set to. Reproduced on
152.0.4-beta.28.

The gate tests the caller, not the world the property is defined in. So export
a getter whose body stays in FrameTree.js's system-principal scope and install
it on the page's own Element.prototype. The default execution context is left
alone: page.evaluate() still runs against the real page window.

Deliberately NOT taking PR daijro#685's shape. It unlocks the same binding by swapping
the default main world for a Cu.Sandbox over the page window, and with Xray
vision that hides page expandos -- its own tests assert
`page.evaluate('window.pageSecret') is None` and
`element.pageMarker is None`. Enabling a shadow-DOM flag should not silently
stop page.evaluate() from seeing page state; that is a much worse failure than
the two caveats below, and it is undocumented in the PR.

The trade-offs of keeping evaluation in the main world, both documented at the
call site: the property is visible to the page while the flag is on (so it can
be fingerprinted -- hence opt-in and off by default), and a page that defines
its own `shadowRootUnl` on an element shadows the accessor. A page can only do
either if it already knows the property exists.

Credit to @Cloudymap1e (daijro#685) for the Cu.exportFunction technique this reuses.

Adds tests/patches/force-scope-access.py, which pins both halves: the binding
works with the flag on, is absent with it off, and main-world evaluation sees
page globals, page expandos and element handles identically in both modes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

shadowRootUnl no longer exposed

1 participant