From c537b74ef11c8aa837b0b8b8ff22c569a57a2adb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E4=B8=80=E4=B9=8B?= Date: Thu, 27 Aug 2026 16:49:46 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=F0=9F=90=9B=20=E4=BF=AE=E5=A4=8D=20Firefox?= =?UTF-8?q?=20content=20world=20=E6=B2=99=E7=9B=92=E7=BC=BA=E5=A4=B1=20Eve?= =?UTF-8?q?ntTarget=20=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Firefox 的 content / USER_SCRIPT world 全局是 Cu.Sandbox:原型链在 Xray window 处 截断,够不到 EventTarget.prototype,导致沙盒收集不到 addEventListener / removeEventListener / dispatchEvent。带 @inject-into content 的脚本调用 globalThis.addEventListener 即抛 TypeError。 改为两趟收集:第一趟仍取 globalThis 保住本 realm 完整的 JS 内置物件(经 Xray 看页面 window 的 Number / Math 会被剥到只剩 length,name,prototype),第二趟取 window 补齐 DOM 原型链。descsCache 先到先得保证优先级;Chrome 下 window === globalThis,第二趟 全部跳过。 --- src/app/service/content/create_context.ts | 101 ++++++++++++---------- 1 file changed, 56 insertions(+), 45 deletions(-) diff --git a/src/app/service/content/create_context.ts b/src/app/service/content/create_context.ts index 539fbd225..bc5eba047 100644 --- a/src/app/service/content/create_context.ts +++ b/src/app/service/content/create_context.ts @@ -183,57 +183,68 @@ const protoBaseDescs: Record = Object.create(null); // 包含物件本身及所有父类(不包含Object)的PropertyDescriptor // 主要是找出哪些 function值, setter/getter 需要替换 global window -getAllPropertyDescriptors(global, ([key, desc]) => { - if (!desc || descsCache.has(key) || typeof key !== "string") return; - - if (desc.writable) { - // 属性 value - - const value = desc.value; - - // 替换 function 的 this 为 实际的 global window - // 例:父类的 addEventListener - // 对于构造函数和类(有 prototype 属性),shouldFnBind 会返回 false,跳过绑定 - // 因此被封装的属性,会略过封装层,继续向父类寻找原生属性 - if (shouldFnBind(value)) { - const boundValue = value.bind(global); - overridedDescs[key] = { - ...desc, - value: boundValue, - }; - descsCache.add(key); // 必须:子类属性覆盖父类属性 - } else if (!(key in initOwnDescs) && !Object.hasOwn(global, key)) { - if (!protoBaseDescs[key]) { - if (typeof value === "function") { - const boundValue = value.bind(global); - protoBaseDescs[key] = { - ...desc, - value: boundValue, - }; - } else { - protoBaseDescs[key] = { ...desc }; - } - } - } - } else { - if (desc.configurable && desc.get && desc.set && desc.enumerable && key.startsWith("on")) { - // 替换 onxxxxx 事件赋值操作 - // 例:(window.)onload, (window.)onerror - eventDescs[key] = desc; - } else { - if (desc.get || desc.set) { - // 替换 getter setter 的 this 为 实际的 global window - // 例:(window.)location, (window.)document +// bind 目标跟随该轮的根物件 root,因为两个根分属不同 realm,互相绑定会触发 brand check 失败 +const collectPropertyDescriptors = (root: any) => + getAllPropertyDescriptors(root, ([key, desc]) => { + if (!desc || descsCache.has(key) || typeof key !== "string") return; + + if (desc.writable) { + // 属性 value + + const value = desc.value; + + // 替换 function 的 this 为 实际的 global window + // 例:父类的 addEventListener + // 对于构造函数和类(有 prototype 属性),shouldFnBind 会返回 false,跳过绑定 + // 因此被封装的属性,会略过封装层,继续向父类寻找原生属性 + if (shouldFnBind(value)) { + const boundValue = value.bind(root); overridedDescs[key] = { ...desc, - get: desc?.get?.bind(global), - set: desc?.set?.bind(global), + value: boundValue, }; descsCache.add(key); // 必须:子类属性覆盖父类属性 + } else if (!(key in initOwnDescs) && !Object.hasOwn(root, key)) { + if (!protoBaseDescs[key]) { + if (typeof value === "function") { + const boundValue = value.bind(root); + protoBaseDescs[key] = { + ...desc, + value: boundValue, + }; + } else { + protoBaseDescs[key] = { ...desc }; + } + } + } + } else { + if (desc.configurable && desc.get && desc.set && desc.enumerable && key.startsWith("on")) { + // 替换 onxxxxx 事件赋值操作 + // 例:(window.)onload, (window.)onerror + eventDescs[key] = desc; + } else { + if (desc.get || desc.set) { + // 替换 getter setter 的 this 为 实际的 global window + // 例:(window.)location, (window.)document + overridedDescs[key] = { + ...desc, + get: desc?.get?.bind(root), + set: desc?.set?.bind(root), + }; + descsCache.add(key); // 必须:子类属性覆盖父类属性 + } } } - } -}); + }); + +// 第一趟 globalThis:Firefox 的 content / USER_SCRIPT world 是独立 realm,JS 内置物件只有在这里 +// 才完整;经 Xray 看页面 window 的内置物件会被剥到只剩 length / name / prototype(Number.isNaN、 +// Math 的全部静态成员都会消失)。 +collectPropertyDescriptors(global); +// 第二趟 window:同一个 sandbox 的原型链在 Xray window 处截断,够不到 EventTarget.prototype, +// addEventListener / removeEventListener / dispatchEvent 只能由真实 window 的原型链补齐。 +// descsCache 先到先得,第一趟收下的键不会被覆盖;Chrome 下 window === globalThis,此趟全部跳过。 +collectPropertyDescriptors(window); descsCache.clear(); // 内存释放 // sharedInitCopy: 完全继承Window.prototype 及 自定义 OwnPropertyDescriptor From b3b85a53c72d8ae328b1f4ef014c023ac9a09adc Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Thu, 27 Aug 2026 20:37:44 +0900 Subject: [PATCH 2/2] Update create_context.ts --- src/app/service/content/create_context.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/service/content/create_context.ts b/src/app/service/content/create_context.ts index bc5eba047..165e6819e 100644 --- a/src/app/service/content/create_context.ts +++ b/src/app/service/content/create_context.ts @@ -244,7 +244,7 @@ collectPropertyDescriptors(global); // 第二趟 window:同一个 sandbox 的原型链在 Xray window 处截断,够不到 EventTarget.prototype, // addEventListener / removeEventListener / dispatchEvent 只能由真实 window 的原型链补齐。 // descsCache 先到先得,第一趟收下的键不会被覆盖;Chrome 下 window === globalThis,此趟全部跳过。 -collectPropertyDescriptors(window); +window !== global && collectPropertyDescriptors(window); descsCache.clear(); // 内存释放 // sharedInitCopy: 完全继承Window.prototype 及 自定义 OwnPropertyDescriptor