Skip to content

fix(beartrap): wrap navigator getters through the Xray waiver + report the outcome - #133

Merged
mdheller merged 9 commits into
mainfrom
fix-beartrap-xray-clamp
Jul 29, 2026
Merged

fix(beartrap): wrap navigator getters through the Xray waiver + report the outcome#133
mdheller merged 9 commits into
mainfrom
fix-beartrap-xray-clamp

Conversation

@mdheller

Copy link
Copy Markdown
Contributor

Why the clamp never worked

The actor runs privileged, so win.Navigator.prototype reached from it is an Xray wrapper — and getOwnPropertyDescriptor through it doesn't return the content getter. The old code hit if (!d.get) return and silently did nothing. No error, no log, no effect.

Everything else was already correct, which is exactly why this was hard to see. Verified in shipped v150.0.2: BearTrapChild is packaged ✅, the clamp code is in it ✅, the pref is on ✅, and the actor is registered in DesktopActorRegistry (matches http+https, allFrames: true, enablePreference set) ✅.

For the record: I first claimed the actor was missing from the registry and published that in the v150.0.2 notes. That was wrong — I grepped browser/omni.ja when the registry ships in the toolkit omni at moz-src/browser/components/. Zero hits from a path that doesn't exist is not evidence of absence. Notes corrected.

The fix

Operate on win.wrappedJSObject (Xray-waived) and install the replacement getter with Cu.exportFunction so it's created in the content compartment — defining a privileged function on a content object would leak chrome into the page.

The part that matters as much as the fix

Failures are no longer swallowed. Each wrap records why it failed, exposed as navigator.__bearTrapWrap:

  • "ok" — wrapping applied
  • "partial:<reasons>" — wrapped some, with the reason
  • undefinedthe actor never ran at all

The audit page reads it, so the next build answers the question instead of us inferring it from a missing side effect.

mdheller added 9 commits July 29, 2026 03:14
Michael: 'zero trust dude we don't trust mozilla, we review the upstream and
harden where possible too.' He is right and my earlier framing was still too
trusting: a valid signature proves only that MOZILLA SIGNED IT, not that the
content is benign. A compelled or compromised upstream ships signed records.

review.mjs reads what we are about to serve and FAILS CLOSED on: a collection
with no signature, a URL in signed content the collection has no business
carrying, active-content markers (javascript:/<script/eval(), a new or vanished
collection, or a >25% swing in record count — the shape a poisoned security list
would take. A SHA256 baseline makes silent drift in already-vouched-for data
visible on the next run.

Made it collection-aware after the first pass produced noise — blunt rules get
ignored, which is worse than no rule. ct-logs IS a list of log endpoints,
intermediates embeds CPS URIs in certificate data, plugins/addons rows carry a
user-facing 'why was this blocked' link. Those are inventoried; everywhere else a
URL is a finding. (Same false-positive trap as calling plugins=5 a leak earlier —
verify what upstream legitimately contains before alarming.)

REAL findings from the first review of Mozilla PROD data:
  - Mozilla ships PLACEHOLDER urls in production CT data:
    https://ct.example.com/bogus/ and .../bogus/ipng/
  - plugin-blocklist rows carry CLEARTEXT http:// advisory links (oracle,
    adobe, microsoft, divx, mozilla blog)
Neither is fetched in normal operation, but we now SEE them instead of assuming,
and the baseline diff surfaces the next change automatically.
…t the outcome

Why the clamp never worked, now understood. The actor runs PRIVILEGED, so
win.Navigator.prototype reached from it is an XRAY WRAPPER, and
getOwnPropertyDescriptor through that wrapper does not return the content
getter. The old code therefore hit  and silently did
nothing — no error, no log, no effect.

Everything else was already correct, which is why this was hard to see: verified
in the shipped v150.0.2 that BearTrapChild IS packaged, the clamp code IS in it,
the pref IS on, and the actor IS registered in DesktopActorRegistry (matches
http+https, allFrames: true, enablePreference set).

(For the record: I first claimed the actor was missing from the registry and put
that in the v150.0.2 release notes. It was WRONG — I grepped browser/omni.ja when
the registry ships in the TOOLKIT omni at moz-src/browser/components/. Zero hits
from a path that does not exist is not evidence of absence. Notes corrected.)

Fix: operate on win.wrappedJSObject (Xray-waived) and install the replacement
getter with Cu.exportFunction so it is created in the CONTENT compartment —
defining a privileged function on a content object would leak chrome into the
page.

And the part that matters as much as the fix: FAILURES ARE NO LONGER SWALLOWED.
Each wrap records why it failed and the result is exposed as
navigator.__bearTrapWrap ('ok' | 'partial:<reasons>' | undefined when the actor
never ran). The audit page now reads it, so the next build ANSWERS the question
instead of us inferring it from a missing side effect.
@mdheller
mdheller merged commit 8400fa3 into main Jul 29, 2026
16 of 17 checks passed
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.

1 participant