diff --git a/scripts/audit/surface-audit.html b/scripts/audit/surface-audit.html index 0d931b8..b94ddbd 100644 --- a/scripts/audit/surface-audit.html +++ b/scripts/audit/surface-audit.html @@ -44,7 +44,10 @@

BearBrowser content-scope surface audit

running…
userAgentData:typeof N.userAgentData, gpu:typeof N.gpu, locks:typeof N.locks, credentials:typeof N.credentials, serviceWorker:typeof N.serviceWorker, scheduling:typeof N.scheduling, virtualKeyboard:typeof N.virtualKeyboard, - privateAttribution:typeof N.privateAttribution + privateAttribution:typeof N.privateAttribution, + // Did BearTrapChild's getter wrapping actually apply? "ok" | "partial:…" | + // undefined (= the actor never ran at all). Removes the guesswork. + bearTrapWrap: N.__bearTrapWrap }; // 3. Canvas / WebGL / Audio fingerprint entropy (is RFP actually spoofing?) try{const c=document.createElement("canvas");c.width=200;c.height=50;const x=c.getContext("2d"); diff --git a/settings/actors/BearTrapChild.sys.mjs b/settings/actors/BearTrapChild.sys.mjs index 79df1c2..12a12b6 100644 --- a/settings/actors/BearTrapChild.sys.mjs +++ b/settings/actors/BearTrapChild.sys.mjs @@ -126,27 +126,61 @@ export class BearTrapChild extends JSWindowActorChild { hardwareConcurrency: v => (typeof v === "number" && v > 2 ? 2 : v), deviceMemory: v => (typeof v === "number" && v > 2 ? 2 : v), }; + // 🔴 Xray. This actor runs PRIVILEGED; `win.Navigator.prototype` reached + // from here is an Xray wrapper, and getOwnPropertyDescriptor through it does + // not hand back the content getter — so the old code hit `if (!d.get) return` + // and SILENTLY did nothing. Verified against the shipped v150.0.2: + // hardwareConcurrency still reported the real core count (8) even though the + // actor is correctly registered (DesktopActorRegistry: matches http+https, + // allFrames, enablePreference on) and the clamp code was present in the + // packaged BearTrapChild. + // Operate on the WAIVED object, and hand back a getter created in the + // CONTENT compartment via Cu.exportFunction — defining a privileged function + // on a content object would leak chrome into the page. + const Cu = Components.utils; + const waived = win.wrappedJSObject; + + // Every failure below is REPORTED, never swallowed. A wrap that silently + // no-ops is exactly how this shipped broken twice. + const wrapFailures = []; const wrapGetter = (obj, prop, surface) => { try { const d = Object.getOwnPropertyDescriptor(obj, prop); - if (!d || !d.get) return; + if (!d || !d.get) { + wrapFailures.push(`${prop}:no-getter-descriptor`); + return; + } const g = d.get; const clamp = CLAMP[prop]; Object.defineProperty(obj, prop, { configurable: true, - get() { + get: Cu.exportFunction(function () { note(surface); const v = g.call(this); return clamp ? clamp(v) : v; - }, + }, obj), }); - } catch (e) {} + } catch (e) { + wrapFailures.push(`${prop}:${e}`); + } }; - const N = win.Navigator && win.Navigator.prototype; + const N = waived && waived.Navigator && waived.Navigator.prototype; wrapGetter(N, "plugins", "plugins"); wrapGetter(N, "mimeTypes", "plugins"); wrapGetter(N, "hardwareConcurrency", "hardware"); wrapGetter(N, "deviceMemory", "hardware"); + // Report the wrap outcome so a silent no-op can never hide again. Read it + // from the page with `navigator.__bearTrapWrap` or via /honeypot. + try { + const status = wrapFailures.length + ? `partial:${wrapFailures.join(",")}` + : "ok"; + Object.defineProperty(N || {}, "__bearTrapWrap", { + configurable: true, + get: Cu.exportFunction(() => status, N || {}), + }); + } catch (e) {} + if (win.document && win.document.fonts) { wrap(win.document.fonts, "check", "fonts"); }