From 3e6807292e5984b3719748895e3399502bcdcd9d Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Sun, 30 Aug 2026 21:54:20 +0900 Subject: [PATCH 01/12] fix sandbox ff spec --- .../service/content/create_context.test.ts | 187 ++++++++- src/app/service/content/create_context.ts | 381 ++++++++++++------ 2 files changed, 445 insertions(+), 123 deletions(-) diff --git a/src/app/service/content/create_context.test.ts b/src/app/service/content/create_context.test.ts index 54961d107..675b28844 100644 --- a/src/app/service/content/create_context.test.ts +++ b/src/app/service/content/create_context.test.ts @@ -1,7 +1,7 @@ import { afterEach, describe, it, expect, vi } from "vitest"; import type { TScriptInfo } from "@App/app/repo/scripts"; import { encodeRValue } from "@App/pkg/utils/message_value"; -import { createContext, createProxyContext, shouldFnBind } from "./create_context"; +import { createContext, createProxyContext, shouldFnBind, type RealmRoots } from "./create_context"; const createScriptInfo = (metadata: Record = {}): TScriptInfo => ({ @@ -32,6 +32,45 @@ const createTestContext = (grants: string[], metadata: Record new Set(grants) ); +const createSplitRealmRoots = (): RealmRoots => { + const realmGlobal = Object.create(null) as Record; + const hostWindow = Object.create(null) as Record; + const eventTarget = new EventTarget(); + + realmGlobal.Node = class RealmNode {}; + realmGlobal.XMLHttpRequest = class RealmXMLHttpRequest {}; + realmGlobal.realmOnly = "realm-value"; + Object.defineProperty(realmGlobal, "realmAccessor", { + configurable: true, + enumerable: true, + get() { + return this === realmGlobal ? "realm-receiver" : "wrong-receiver"; + }, + }); + + hostWindow.constructor = window.constructor; + hostWindow.EventTarget = EventTarget; + hostWindow.Node = Node; + hostWindow.NodeFilter = NodeFilter; + hostWindow.Event = Event; + hostWindow.XMLHttpRequest = class HostXMLHttpRequest { + static DONE = 4; + }; + hostWindow.document = document; + hostWindow.hostOnly = "host-value"; + hostWindow.addEventListener = eventTarget.addEventListener.bind(eventTarget); + hostWindow.removeEventListener = eventTarget.removeEventListener.bind(eventTarget); + hostWindow.dispatchEvent = eventTarget.dispatchEvent.bind(eventTarget); + Object.defineProperty(hostWindow, "onload", { + configurable: true, + enumerable: true, + get: () => null, + set: () => undefined, + }); + + return { realmGlobal, hostWindow }; +}; + describe.concurrent("shouldFnBind", () => { it.concurrent("不处理非原生函数", () => { const o: Record = {}; @@ -237,4 +276,150 @@ describe.concurrent("createProxyContext", () => { const sandbox = createProxyContext(createTestContext([])); expect(Object.hasOwn(sandbox, "addEventListener")).toBe(true); }); + + describe.concurrent("split-global materialization", () => { + it.concurrent("materializes separate realm and host roots through the eager snapshot", () => { + const roots = createSplitRealmRoots(); + const sandbox = createProxyContext(createTestContext([]), roots); + + expect(sandbox.Node).toBe(roots.hostWindow.Node); + expect(sandbox.Node.prototype).toBe(roots.hostWindow.Node.prototype); + expect(sandbox.XMLHttpRequest).toBe(roots.hostWindow.XMLHttpRequest); + expect(sandbox.XMLHttpRequest.DONE).toBe(4); + expect(sandbox.EventTarget).toBe(roots.hostWindow.EventTarget); + expect(sandbox.document).toBe(document); + expect(sandbox.realmOnly).toBe("realm-value"); + expect(sandbox.realmAccessor).toBe("realm-receiver"); + expect(sandbox.hostOnly).toBeUndefined(); + + const listener = vi.fn(); + sandbox.addEventListener("split-root", listener); + roots.hostWindow.dispatchEvent(new Event("split-root")); + expect(listener).toHaveBeenCalledTimes(1); + sandbox.removeEventListener("split-root", listener); + + const onload = vi.fn(); + Reflect.set(sandbox, "onload", onload); + roots.hostWindow.dispatchEvent(new Event("load")); + Reflect.set(sandbox, "onload", null); + roots.hostWindow.dispatchEvent(new Event("load")); + expect(onload).toHaveBeenCalledTimes(1); + }); + + it.concurrent("keeps all self-referential window names inside the sandbox", () => { + const sandbox = createProxyContext(createTestContext([])); + + expect(sandbox.window).toBe(sandbox); + expect(sandbox.self).toBe(sandbox); + expect(sandbox.globalThis).toBe(sandbox); + expect(sandbox.top).toBe(sandbox); + expect(sandbox.parent).toBe(sandbox); + expect(sandbox.frames).toBe(sandbox); + }); + + it.concurrent("forwards live host accessors without leaking page globals", () => { + const sandbox = createProxyContext(createTestContext([])); + const pageKey = "__scriptcat_split_global_page_value"; + + Reflect.set(window, pageKey, "page-value"); + try { + expect(sandbox.document).toBe(window.document); + expect(sandbox.location).toBe(window.location); + expect(sandbox[pageKey]).toBeUndefined(); + } finally { + Reflect.deleteProperty(window, pageKey); + } + }); + + it.concurrent("preserves host constructor static constants and prototype identity", () => { + const sandbox = createProxyContext(createTestContext([])); + + expect(sandbox.Node).toBe(window.Node); + expect(sandbox.Node.ELEMENT_NODE).toBe(window.Node.ELEMENT_NODE); + expect(sandbox.Node.prototype).toBe(window.Node.prototype); + expect(sandbox.Event).toBe(window.Event); + expect(sandbox.Event.prototype).toBe(window.Event.prototype); + }); + + it.concurrent("keeps JavaScript built-in static methods available", () => { + const sandbox = createProxyContext(createTestContext([])); + + expect(sandbox.Number.isNaN).toBe(Number.isNaN); + expect(sandbox.Math.max(2, 7)).toBe(7); + expect(sandbox.Object.isFrozen(Object.freeze({}))).toBe(true); + }); + + it.concurrent("forwards extracted host methods with the host receiver", () => { + const sandbox = createProxyContext(createTestContext([])); + const add = sandbox.addEventListener; + const remove = sandbox.removeEventListener; + const dispatch = sandbox.dispatchEvent; + const eventName = "__scriptcat_split_global_event"; + const listener = vi.fn(); + + add(eventName, listener); + dispatch(new Event(eventName)); + remove(eventName, listener); + + expect(listener).toHaveBeenCalledTimes(1); + }); + + it.concurrent("does not register an event listener for an object handler", () => { + const sandbox = createProxyContext(createTestContext([])); + const listenerObject = { handleEvent: vi.fn() }; + + Reflect.set(sandbox, "onfocus", listenerObject); + window.dispatchEvent(new Event("focus")); + + expect(listenerObject.handleEvent).not.toHaveBeenCalled(); + sandbox.onfocus = null; + }); + + it.concurrent("removes the old event listener when an on-property is cleared", () => { + const sandbox = createProxyContext(createTestContext([])); + const handler = vi.fn(); + + sandbox.onresize = handler; + window.dispatchEvent(new Event("resize")); + sandbox.onresize = null; + window.dispatchEvent(new Event("resize")); + + expect(handler).toHaveBeenCalledTimes(1); + }); + + it.concurrent("replaces an on-property listener without retaining the previous callback", () => { + const sandbox = createProxyContext(createTestContext([])); + const oldHandler = vi.fn(); + const newHandler = vi.fn(); + + try { + sandbox.onhashchange = oldHandler; + sandbox.onhashchange = newHandler; + window.dispatchEvent(new Event("hashchange")); + + expect(oldHandler).not.toHaveBeenCalled(); + expect(newHandler).toHaveBeenCalledTimes(1); + } finally { + sandbox.onhashchange = null; + } + }); + + it.concurrent("isolates writes between split-global sandboxes", () => { + const first = createProxyContext(createTestContext([])); + const second = createProxyContext(createTestContext([])); + + first.__split_global_local_value = "first"; + + expect(first.__split_global_local_value).toBe("first"); + expect(second.__split_global_local_value).toBeUndefined(); + expect(Reflect.get(window, "__split_global_local_value")).toBeUndefined(); + }); + + it.concurrent("keeps the page window identity separate from the sandbox identity", () => { + const sandbox = createProxyContext(createTestContext([])); + + expect(sandbox).not.toBe(window); + expect(sandbox.unsafeWindow).toBe(window); + }); + }); }); diff --git a/src/app/service/content/create_context.ts b/src/app/service/content/create_context.ts index 165e6819e..7097a3ca4 100644 --- a/src/app/service/content/create_context.ts +++ b/src/app/service/content/create_context.ts @@ -155,39 +155,166 @@ export const shouldFnBind = (f: any) => { return false; }; -type ForEachCallback = (value: T, index: number, array: T[]) => void; - // 取物件本身及所有父类(不包含Object)的PropertyDescriptor -const getAllPropertyDescriptors = (obj: any, callback: ForEachCallback<[string | symbol, PropertyDescriptor]>) => { +type DescriptorOwner = Record; +type DescriptorEntry = [string | symbol, PropertyDescriptor, DescriptorOwner]; +type TrackedDescriptor = { + descriptor: PropertyDescriptor; + owner: DescriptorOwner; + receiver: DescriptorOwner; + isConstructor: boolean; +}; + +const getAllPropertyDescriptors = (obj: DescriptorOwner, callback: (value: DescriptorEntry) => void) => { while (obj && obj !== Object) { const descs = Object.getOwnPropertyDescriptors(obj); - Object.entries(descs).forEach(callback); + Reflect.ownKeys(descs).forEach((key) => callback([key, descs[key as keyof typeof descs], obj])); obj = Object.getPrototypeOf(obj); } }; -// 在 CacheSet 加入的propKeys将会在 mySandbox 实装阶段时设置 -const descsCache: Set = new Set(["eval", "window", "self", "globalThis", "top", "parent"]); +// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type +const isConstructorOrInterface = (value: unknown): value is Function => + typeof value === "function" && ("prototype" in value || /^[A-Z]/.test(String(Reflect.get(value, "name")))); -const initOwnDescs = Object.getOwnPropertyDescriptors(global); +const materializeDescriptor = (key: string, tracked: TrackedDescriptor): PropertyDescriptor => { + const { descriptor, owner, receiver, isConstructor } = tracked; + if ("value" in descriptor) { + if (typeof descriptor.value !== "function" || isConstructor) return { ...descriptor }; + return { + ...descriptor, + value: function (this: any, ...args: any[]) { + return Reflect.apply(descriptor.value, receiver, args); + }, + }; + } + return { + ...descriptor, + get: descriptor.get ? () => Reflect.get(owner, key, receiver) : undefined, + set: descriptor.set + ? (value: any) => { + Reflect.set(owner, key, value, receiver); + } + : undefined, + }; +}; -// overridedDescs将以物件OwnPropertyDescriptor方式进行物件属性修改 -// 覆盖原有的 OwnPropertyDescriptor定义 或 父类的PropertyDescriptor定义 -const overridedDescs: Record = Object.create(null); +// Firefox 的 content / USER_SCRIPT world 将 JavaScript global 与页面 window 拆成两个 realm。 +// 这里仅转发确定需要页面 brand 的成员;不遍历第二个根,避免把页面全局快照带入沙盒。 +const hostWindowAccessors = [ + "document", + "location", + "navigator", + "history", + "screen", + "performance", + "crypto", + "localStorage", + "sessionStorage", + "visualViewport", + "innerWidth", + "innerHeight", + "scrollX", + "scrollY", + "devicePixelRatio", +]; +const hostWindowMethods = [ + "addEventListener", + "removeEventListener", + "dispatchEvent", + "getComputedStyle", + "matchMedia", + "requestAnimationFrame", + "cancelAnimationFrame", + "scroll", + "scrollTo", + "scrollBy", + "blur", +]; +const hostWindowConstructors = [ + "Window", + "EventTarget", + "Node", + "Element", + "HTMLElement", + "Document", + "DocumentFragment", + "ShadowRoot", + "Text", + "Range", + "MutationObserver", + "NodeFilter", + "TreeWalker", + "Event", + "CustomEvent", + "MouseEvent", + "KeyboardEvent", + "PointerEvent", + "InputEvent", + "FocusEvent", + "ErrorEvent", + "ProgressEvent", + "MessageEvent", + "StorageEvent", + "WheelEvent", + "DragEvent", + "ClipboardEvent", + "DOMParser", + "XMLSerializer", + "FormData", + "File", + "FileList", + "Blob", + "URL", + "URLSearchParams", + "Headers", + "Request", + "Response", + "XMLHttpRequest", +]; +const hostWindowEventProperties = ["onload", "onerror", "onresize", "onfocus", "onblur", "onhashchange"]; + +const hostWindowKeys = new Set([...hostWindowAccessors, ...hostWindowMethods, ...hostWindowConstructors]); + +type GlobalSnapshot = { + sharedInitCopy: typeof globalThis & Record; + eventDescs: Record; +}; -// 记录原生 onxxxxx 的 PropertyDescriptor -const eventDescs: Record = Object.create(null); +export type RealmRoots = { + realmGlobal: DescriptorOwner; + hostWindow: DescriptorOwner; +}; -// 在 USE_PSEUDO_WINDOW 情况下,由于没有 类的prototype, 父类的成员要手动传下去 -const protoBaseDescs: Record = Object.create(null); +const createGlobalSnapshot = ({ realmGlobal, hostWindow }: RealmRoots): GlobalSnapshot => { + const descsCache: Set = new Set(["eval", "window", "self", "globalThis", "top", "parent"]); + const initOwnDescs = Object.getOwnPropertyDescriptors(realmGlobal); + const overridedDescs: Record = Object.create(null); + const eventDescs: Record = Object.create(null); + const protoBaseDescs: Record = Object.create(null); + + const getHostWindowDescriptor = (key: string): PropertyDescriptor | undefined => { + let owner: DescriptorOwner | null = hostWindow; + while (owner) { + const descriptor = Object.getOwnPropertyDescriptor(owner, key); + if (descriptor) return descriptor; + owner = Object.getPrototypeOf(owner); + } + return undefined; + }; -// 包含物件本身及所有父类(不包含Object)的PropertyDescriptor -// 主要是找出哪些 function值, setter/getter 需要替换 global window -// bind 目标跟随该轮的根物件 root,因为两个根分属不同 realm,互相绑定会触发 brand check 失败 -const collectPropertyDescriptors = (root: any) => - getAllPropertyDescriptors(root, ([key, desc]) => { + // 包含物件本身及所有父类(不包含Object)的PropertyDescriptor + // 主要是找出哪些 function值, setter/getter 需要替换 global window + getAllPropertyDescriptors(realmGlobal, ([key, desc, owner]) => { if (!desc || descsCache.has(key) || typeof key !== "string") return; + const tracked = { + descriptor: desc, + owner, + receiver: realmGlobal, + isConstructor: isConstructorOrInterface(desc.value), + }; + if (desc.writable) { // 属性 value @@ -198,23 +325,11 @@ const collectPropertyDescriptors = (root: any) => // 对于构造函数和类(有 prototype 属性),shouldFnBind 会返回 false,跳过绑定 // 因此被封装的属性,会略过封装层,继续向父类寻找原生属性 if (shouldFnBind(value)) { - const boundValue = value.bind(root); - overridedDescs[key] = { - ...desc, - value: boundValue, - }; + overridedDescs[key] = materializeDescriptor(key, tracked); descsCache.add(key); // 必须:子类属性覆盖父类属性 - } else if (!(key in initOwnDescs) && !Object.hasOwn(root, key)) { + } else if (!(key in initOwnDescs) && !Object.hasOwn(realmGlobal, key)) { if (!protoBaseDescs[key]) { - if (typeof value === "function") { - const boundValue = value.bind(root); - protoBaseDescs[key] = { - ...desc, - value: boundValue, - }; - } else { - protoBaseDescs[key] = { ...desc }; - } + protoBaseDescs[key] = materializeDescriptor(key, tracked); } } } else { @@ -226,68 +341,101 @@ const collectPropertyDescriptors = (root: any) => 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), - }; + overridedDescs[key] = materializeDescriptor(key, tracked); descsCache.add(key); // 必须:子类属性覆盖父类属性 } } } }); + descsCache.clear(); // 内存释放 + + const addHostWindowForwarding = (key: string) => { + if (!(key in hostWindow)) return; + const hostDescriptor = getHostWindowDescriptor(key); + if (hostWindowAccessors.includes(key)) { + overridedDescs[key] = { + configurable: true, + enumerable: true, + get: () => Reflect.get(hostWindow, key, hostWindow), + ...(hostDescriptor?.set + ? { + set: (value: any) => { + Reflect.set(hostWindow, key, value, hostWindow); + }, + } + : {}), + }; + } else if (hostWindowMethods.includes(key)) { + overridedDescs[key] = { + configurable: true, + enumerable: true, + writable: true, + value: function (...args: any[]) { + const method = Reflect.get(hostWindow, key, hostWindow); + return Reflect.apply(method, hostWindow, args); + }, + }; + } else { + overridedDescs[key] = { + configurable: true, + enumerable: true, + writable: true, + value: Reflect.get(hostWindow, key, hostWindow), + }; + } + }; -// 第一趟 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,此趟全部跳过。 -window !== global && collectPropertyDescriptors(window); -descsCache.clear(); // 内存释放 - -// sharedInitCopy: 完全继承Window.prototype 及 自定义 OwnPropertyDescriptor -// OwnPropertyDescriptor定义 为 原OwnPropertyDescriptor定义 (DragEvent, MouseEvent, RegExp, EventTarget, JSON等) -// + 覆盖定义 (document, location, setTimeout, setInterval, addEventListener 等) -// sharedInitCopy: ScriptCat脚本共通使用 - -const USE_PSEUDO_WINDOW = true; // 日后或能设置使 ScriptCat的沙盒 window 能以 name / id 存取页面元素 - -class PseudoWindow {} -const PseudoWindowPrototype = PseudoWindow.prototype; -Object.defineProperty(PseudoWindowPrototype, Symbol.toStringTag, { - //@ts-ignore - value: global[Symbol.toStringTag], - writable: false, - enumerable: false, - configurable: true, -}); -Object.defineProperty(PseudoWindowPrototype, "constructor", { - value: global.constructor, - writable: false, - enumerable: false, - configurable: true, -}); -Object.defineProperty(PseudoWindowPrototype, "__proto__", { - //@ts-ignore - value: global.__proto__, - writable: false, - enumerable: false, - configurable: true, -}); - -const sharedInitCopy = USE_PSEUDO_WINDOW - ? Object.create(null, { - ...protoBaseDescs, // 较快的 @unwrap 注入时有机会改变 EventTarget.prototype - ...Object.getOwnPropertyDescriptors(PseudoWindowPrototype), - ...initOwnDescs, - ...overridedDescs, - }) - : Object.create(Object.getPrototypeOf(global), { - ...initOwnDescs, - ...overridedDescs, - }); + for (const key of hostWindowKeys) addHostWindowForwarding(key); + for (const key of hostWindowEventProperties) { + if (key in hostWindow && !eventDescs[key]) eventDescs[key] = { configurable: true, enumerable: true }; + } + + // sharedInitCopy: 完全继承Window.prototype 及 自定义 OwnPropertyDescriptor + // OwnPropertyDescriptor定义 为 原OwnPropertyDescriptor定义 (DragEvent, MouseEvent, RegExp, EventTarget, JSON等) + // + 覆盖定义 (document, location, setTimeout, setInterval, addEventListener 等) + // sharedInitCopy: ScriptCat脚本共通使用 + + const USE_PSEUDO_WINDOW = true; // 日后或能设置使 ScriptCat的沙盒 window 能以 name / id 存取页面元素 + + class PseudoWindow {} + const PseudoWindowPrototype = PseudoWindow.prototype; + Object.defineProperty(PseudoWindowPrototype, Symbol.toStringTag, { + //@ts-ignore + value: hostWindow[Symbol.toStringTag], + writable: false, + enumerable: false, + configurable: true, + }); + Object.defineProperty(PseudoWindowPrototype, "constructor", { + value: hostWindow.constructor, + writable: false, + enumerable: false, + configurable: true, + }); + Object.defineProperty(PseudoWindowPrototype, "__proto__", { + //@ts-ignore + value: hostWindow.__proto__, + writable: false, + enumerable: false, + configurable: true, + }); + + const sharedInitCopy = USE_PSEUDO_WINDOW + ? Object.create(null, { + ...protoBaseDescs, // 较快的 @unwrap 注入时有机会改变 EventTarget.prototype + ...Object.getOwnPropertyDescriptors(PseudoWindowPrototype), + ...initOwnDescs, + ...overridedDescs, + }) + : Object.create(Object.getPrototypeOf(realmGlobal), { + ...initOwnDescs, + ...overridedDescs, + }); + + return { sharedInitCopy, eventDescs }; +}; + +const defaultGlobalSnapshot = createGlobalSnapshot({ realmGlobal: global, hostWindow: window }); // 把沙盒的 console 和网页的 console 隔离 const initConsoleDescs = Object.getOwnPropertyDescriptors(console); @@ -298,22 +446,21 @@ type GMWorldContext = typeof globalThis & Record; const isPrimitive = (x: any) => x !== Object(x); // 拦截上下文 -export const createProxyContext = (context: any): Context => { +export const createProxyContext = ( + context: any, + roots: RealmRoots = { realmGlobal: global, hostWindow: window } +): Context => { // let withContext: Context | undefined | { [key: string]: any } = undefined; // 为避免做成混乱。 ScriptCat脚本中 self, globalThis, parent 为固定值不能修改 + const { sharedInitCopy, eventDescs } = + roots.realmGlobal === global && roots.hostWindow === window ? defaultGlobalSnapshot : createGlobalSnapshot(roots); const ownDescs = Object.getOwnPropertyDescriptors(sharedInitCopy); // mySandbox: ScriptCat各脚本独自使用 let mySandbox: typeof sharedInitCopy | undefined = undefined; - - const createFuncWrapper = (f: () => any) => { - return function (this: any) { - const ret = f.call(global); - if (ret === global) return mySandbox; - return ret; - }; - }; + const hostAddEventListener = roots.hostWindow.addEventListener.bind(roots.hostWindow); + const hostRemoveEventListener = roots.hostWindow.removeEventListener.bind(roots.hostWindow); // 用 eventHandling 机制模拟 onxxxxxxx 事件设置 // 监听事件实际上的方法是eventObject.handleEvent @@ -323,9 +470,9 @@ export const createProxyContext = (context const eventObject: EventListenerObject & { fn: any } = { fn: null, handleEvent(event) { - const fn = mySandbox[key]; + const fn = mySandbox![key]; if (!fn || fn !== this.fn) { - global.removeEventListener(eventName, eventObject); + hostRemoveEventListener(eventName, eventObject); this.fn = null; } else { fn.call(mySandbox, event); @@ -347,11 +494,11 @@ export const createProxyContext = (context // function <-> function 时无需重新监听 if (typeof fn === "function") { // 停止当前事件监听 - global.removeEventListener(eventName, eventObject); + hostRemoveEventListener(eventName, eventObject); } else if (typeof newVal === "function") { // 非primitive types 的话,只考虑 function type // Symbol, Object (包括 EventListenerObject ) 等只会保存而不进行事件监听 - global.addEventListener(eventName, eventObject); + hostAddEventListener(eventName, eventObject); } } eventObject.fn = newVal; @@ -369,24 +516,13 @@ export const createProxyContext = (context } for (const key of ["window", "self", "globalThis", "top", "parent", "frames"]) { - const desc = ownDescs[key]; - if (desc?.value === global) { - // globalThis - // 避免 self referencing, 改以 getter 形式 - desc.get = function () { + ownDescs[key] = { + configurable: true, + enumerable: true, + get() { return mySandbox; - }; - desc.set = undefined; - // 为了 value 转 getter/setter,必须删除 writable 和 value - delete desc.writable; - delete desc.value; - } else if (desc?.get) { - // 真实的 window 物件中部份属性(self, parent) 存在setter. 意义不明 - // 为避免做成混乱,ScriptCat脚本的沙盒不提供setter(即不能修改) - // (像window.document, 能写 window.document = null 不会报错但赋值不变) - desc.get = createFuncWrapper(desc.get); - desc.set = undefined; - } + }, + }; } if (noEval) { if (ownDescs?.eval?.value) { @@ -422,7 +558,8 @@ export const createProxyContext = (context } // 把初始Copy加上特殊变量后,生成一份新Copy - mySandbox = Object.create(Object.getPrototypeOf(sharedInitCopy), ownDescs); + mySandbox = Object.create(Object.getPrototypeOf(sharedInitCopy), ownDescs) as typeof globalThis & + Record; // 处理特殊关键字,不能穿越出沙盒,也不能被外部修改 for (const key of ["define", "module", "exports"]) { @@ -457,7 +594,7 @@ export const createProxyContext = (context const handle = function (this: Window & Record, e: UrlChangeEvent) { this.onurlchange?.(e); } as EventListener; - (window).addEventListener("urlchange", handle.bind(mySandbox), false); + (roots.hostWindow).addEventListener("urlchange", handle.bind(mySandbox), false); } // 从网页 console 隔离出来的沙盒 console From a63891090c05035f22cd7a68509428f178c61515 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Sun, 30 Aug 2026 22:11:07 +0900 Subject: [PATCH 02/12] fix --- .../service/content/create_context.test.ts | 14 +++++ src/app/service/content/create_context.ts | 57 ++++++++++--------- 2 files changed, 43 insertions(+), 28 deletions(-) diff --git a/src/app/service/content/create_context.test.ts b/src/app/service/content/create_context.test.ts index 675b28844..5574f0ec7 100644 --- a/src/app/service/content/create_context.test.ts +++ b/src/app/service/content/create_context.test.ts @@ -67,6 +67,12 @@ const createSplitRealmRoots = (): RealmRoots => { get: () => null, set: () => undefined, }); + Object.defineProperty(hostWindow, "oncustomcompat", { + configurable: true, + enumerable: true, + get: () => null, + set: () => undefined, + }); return { realmGlobal, hostWindow }; }; @@ -250,6 +256,8 @@ describe.concurrent("createProxyContext", () => { const sandbox = createProxyContext(createTestContext([])); const setTimeoutForTest1 = sandbox.setTimeoutForTest1; + expect(setTimeoutForTest1.name).toBe("bound setTimeoutForTest1"); + expect("prototype" in setTimeoutForTest1).toBe(false); expect(() => setTimeoutForTest1(() => undefined, 0)).not.toThrow(); }); @@ -304,6 +312,12 @@ describe.concurrent("createProxyContext", () => { Reflect.set(sandbox, "onload", null); roots.hostWindow.dispatchEvent(new Event("load")); expect(onload).toHaveBeenCalledTimes(1); + + const customCompatHandler = vi.fn(); + Reflect.set(sandbox, "oncustomcompat", customCompatHandler); + roots.hostWindow.dispatchEvent(new Event("customcompat")); + Reflect.set(sandbox, "oncustomcompat", null); + expect(customCompatHandler).toHaveBeenCalledTimes(1); }); it.concurrent("keeps all self-referential window names inside the sandbox", () => { diff --git a/src/app/service/content/create_context.ts b/src/app/service/content/create_context.ts index 7097a3ca4..5e29c16fc 100644 --- a/src/app/service/content/create_context.ts +++ b/src/app/service/content/create_context.ts @@ -157,10 +157,9 @@ export const shouldFnBind = (f: any) => { // 取物件本身及所有父类(不包含Object)的PropertyDescriptor type DescriptorOwner = Record; -type DescriptorEntry = [string | symbol, PropertyDescriptor, DescriptorOwner]; +type DescriptorEntry = [string | symbol, PropertyDescriptor]; type TrackedDescriptor = { descriptor: PropertyDescriptor; - owner: DescriptorOwner; receiver: DescriptorOwner; isConstructor: boolean; }; @@ -168,7 +167,7 @@ type TrackedDescriptor = { const getAllPropertyDescriptors = (obj: DescriptorOwner, callback: (value: DescriptorEntry) => void) => { while (obj && obj !== Object) { const descs = Object.getOwnPropertyDescriptors(obj); - Reflect.ownKeys(descs).forEach((key) => callback([key, descs[key as keyof typeof descs], obj])); + Reflect.ownKeys(descs).forEach((key) => callback([key, descs[key as keyof typeof descs]])); obj = Object.getPrototypeOf(obj); } }; @@ -177,25 +176,19 @@ const getAllPropertyDescriptors = (obj: DescriptorOwner, callback: (value: Descr const isConstructorOrInterface = (value: unknown): value is Function => typeof value === "function" && ("prototype" in value || /^[A-Z]/.test(String(Reflect.get(value, "name")))); -const materializeDescriptor = (key: string, tracked: TrackedDescriptor): PropertyDescriptor => { - const { descriptor, owner, receiver, isConstructor } = tracked; +const materializeDescriptor = (tracked: TrackedDescriptor): PropertyDescriptor => { + const { descriptor, receiver, isConstructor } = tracked; if ("value" in descriptor) { if (typeof descriptor.value !== "function" || isConstructor) return { ...descriptor }; return { ...descriptor, - value: function (this: any, ...args: any[]) { - return Reflect.apply(descriptor.value, receiver, args); - }, + value: Function.prototype.bind.call(descriptor.value, receiver), }; } return { ...descriptor, - get: descriptor.get ? () => Reflect.get(owner, key, receiver) : undefined, - set: descriptor.set - ? (value: any) => { - Reflect.set(owner, key, value, receiver); - } - : undefined, + get: descriptor.get?.bind(receiver), + set: descriptor.set?.bind(receiver), }; }; @@ -272,8 +265,6 @@ const hostWindowConstructors = [ "Response", "XMLHttpRequest", ]; -const hostWindowEventProperties = ["onload", "onerror", "onresize", "onfocus", "onblur", "onhashchange"]; - const hostWindowKeys = new Set([...hostWindowAccessors, ...hostWindowMethods, ...hostWindowConstructors]); type GlobalSnapshot = { @@ -305,12 +296,11 @@ const createGlobalSnapshot = ({ realmGlobal, hostWindow }: RealmRoots): GlobalSn // 包含物件本身及所有父类(不包含Object)的PropertyDescriptor // 主要是找出哪些 function值, setter/getter 需要替换 global window - getAllPropertyDescriptors(realmGlobal, ([key, desc, owner]) => { + getAllPropertyDescriptors(realmGlobal, ([key, desc]) => { if (!desc || descsCache.has(key) || typeof key !== "string") return; const tracked = { descriptor: desc, - owner, receiver: realmGlobal, isConstructor: isConstructorOrInterface(desc.value), }; @@ -325,11 +315,11 @@ const createGlobalSnapshot = ({ realmGlobal, hostWindow }: RealmRoots): GlobalSn // 对于构造函数和类(有 prototype 属性),shouldFnBind 会返回 false,跳过绑定 // 因此被封装的属性,会略过封装层,继续向父类寻找原生属性 if (shouldFnBind(value)) { - overridedDescs[key] = materializeDescriptor(key, tracked); + overridedDescs[key] = materializeDescriptor(tracked); descsCache.add(key); // 必须:子类属性覆盖父类属性 } else if (!(key in initOwnDescs) && !Object.hasOwn(realmGlobal, key)) { if (!protoBaseDescs[key]) { - protoBaseDescs[key] = materializeDescriptor(key, tracked); + protoBaseDescs[key] = materializeDescriptor(tracked); } } } else { @@ -341,7 +331,7 @@ const createGlobalSnapshot = ({ realmGlobal, hostWindow }: RealmRoots): GlobalSn if (desc.get || desc.set) { // 替换 getter setter 的 this 为 实际的 global window // 例:(window.)location, (window.)document - overridedDescs[key] = materializeDescriptor(key, tracked); + overridedDescs[key] = materializeDescriptor(tracked); descsCache.add(key); // 必须:子类属性覆盖父类属性 } } @@ -349,6 +339,22 @@ const createGlobalSnapshot = ({ realmGlobal, hostWindow }: RealmRoots): GlobalSn }); descsCache.clear(); // 内存释放 + const hostEventKeys = new Set(); + getAllPropertyDescriptors(hostWindow, ([key, desc]) => { + if ( + typeof key !== "string" || + hostEventKeys.has(key) || + !key.startsWith("on") || + !desc.configurable || + !desc.get || + !desc.set + ) { + return; + } + eventDescs[key] = desc; + hostEventKeys.add(key); + }); + const addHostWindowForwarding = (key: string) => { if (!(key in hostWindow)) return; const hostDescriptor = getHostWindowDescriptor(key); @@ -366,14 +372,12 @@ const createGlobalSnapshot = ({ realmGlobal, hostWindow }: RealmRoots): GlobalSn : {}), }; } else if (hostWindowMethods.includes(key)) { + const method = Reflect.get(hostWindow, key, hostWindow); overridedDescs[key] = { configurable: true, enumerable: true, writable: true, - value: function (...args: any[]) { - const method = Reflect.get(hostWindow, key, hostWindow); - return Reflect.apply(method, hostWindow, args); - }, + value: typeof method === "function" ? Function.prototype.bind.call(method, hostWindow) : method, }; } else { overridedDescs[key] = { @@ -386,9 +390,6 @@ const createGlobalSnapshot = ({ realmGlobal, hostWindow }: RealmRoots): GlobalSn }; for (const key of hostWindowKeys) addHostWindowForwarding(key); - for (const key of hostWindowEventProperties) { - if (key in hostWindow && !eventDescs[key]) eventDescs[key] = { configurable: true, enumerable: true }; - } // sharedInitCopy: 完全继承Window.prototype 及 自定义 OwnPropertyDescriptor // OwnPropertyDescriptor定义 为 原OwnPropertyDescriptor定义 (DragEvent, MouseEvent, RegExp, EventTarget, JSON等) From 7c54365f649ee80206a22fbfbee423a8db65bbfe Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Sun, 30 Aug 2026 22:23:35 +0900 Subject: [PATCH 03/12] =?UTF-8?q?vitest:=20Firefox=20content=20world?= =?UTF-8?q?=EF=BC=9AglobalThis=20=E4=B8=8E=20window=20=E5=88=86=E5=B1=9E?= =?UTF-8?q?=E4=B8=8D=E5=90=8C=20realm?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/content/create_context.test.ts | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) diff --git a/src/app/service/content/create_context.test.ts b/src/app/service/content/create_context.test.ts index 5574f0ec7..f1798b01d 100644 --- a/src/app/service/content/create_context.test.ts +++ b/src/app/service/content/create_context.test.ts @@ -285,6 +285,105 @@ describe.concurrent("createProxyContext", () => { expect(Object.hasOwn(sandbox, "addEventListener")).toBe(true); }); + // Firefox 的 content / USER_SCRIPT world 全局是 Cu.Sandbox:globalThis 与 window 分属两个 realm, + // 沙盒的原型链在 Xray window 处截断,EventTarget.prototype 上的成员只能经 window 取得。 + // happy-dom 里 globalThis === window,只能用一个「仅存在于 window 原型链上」的成员模拟该拓扑。 + describe("Firefox content world:globalThis 与 window 分属不同 realm", () => { + afterEach(() => { + vi.unstubAllGlobals(); + vi.resetModules(); + }); + + it("沙盒补齐只能经 window 原型链取得的成员 (#1692)", async () => { + const windowProto = Object.create(null); + // 原生 DOM 方法没有 prototype,这里必须用同样形状(方法简写),否则模型不成立 + windowProto.onlyReachableViaWindow = { + onlyReachableViaWindow(this: unknown) { + return this; + }, + }.onlyReachableViaWindow; + const fakeWindow = Object.create(windowProto); + vi.stubGlobal("window", fakeWindow); + vi.resetModules(); + + const module = await import("./create_context.js"); + const context = module.createContext( + createScriptInfo(), + { script: { name: "create-context-test" }, scriptMetaStr: "" }, + "vitest", + undefined as any, + undefined as any, + new Set() + ); + const sandbox = module.createProxyContext(context); + + expect(typeof sandbox.onlyReachableViaWindow).toBe("function"); + // bind 目标必须跟随该轮的根物件,否则跨 realm 呼叫会触发 brand check 失败 + expect(sandbox.onlyReachableViaWindow()).toBe(fakeWindow); + }); + + it("接口物件保留 prototype 与静态常量,不被 bind 剥空", async () => { + // bind 的产物没有 prototype、也丢掉全部静态成员。Firefox 的 Cu.Sandbox 上 + // Node / NodeFilter 之类不是自有属性,会走到 protoBaseDescs 分支, + // 无差别 bind 会让 Node.ELEMENT_NODE / NodeFilter.SHOW_TEXT 全变成 undefined。 + const windowProto = Object.create(null); + // 构造函数形状(Node、Event、XMLHttpRequest) + const NodeLike = function NodeLike() {}; + (NodeLike as any).ELEMENT_NODE = 1; + windowProto.NodeLike = NodeLike; + // 回调接口形状(NodeFilter):大写字头但没有 prototype + const FilterLike = () => undefined; + (FilterLike as any).SHOW_TEXT = 4; + windowProto.FilterLike = FilterLike; + + const fakeWindow = Object.create(windowProto); + vi.stubGlobal("window", fakeWindow); + vi.resetModules(); + + const module = await import("./create_context.js"); + const sandbox = module.createProxyContext( + module.createContext( + createScriptInfo(), + { script: { name: "create-context-test" }, scriptMetaStr: "" }, + "vitest", + undefined as any, + undefined as any, + new Set() + ) + ); + + expect(sandbox.NodeLike.ELEMENT_NODE).toBe(1); + expect(sandbox.NodeLike.prototype).toBe(NodeLike.prototype); + expect(sandbox.FilterLike.SHOW_TEXT).toBe(4); + }); + + it("window / self 指向沙盒自身,不逃逸到页面 window", async () => { + // Firefox 下 globalThis.window 是页面 Window 的 Xray 包装,不等于 global; + // 只按 global 判定自引用会让沙盒里的 window / self 指回页面, + // 脚本写在 self 上的东西(例如沉浸式翻译的 GM_fetch)就落到了页面而不是沙盒。 + const pageWindow: Record = Object.create(null); + pageWindow.window = pageWindow; + pageWindow.self = pageWindow; + vi.stubGlobal("window", pageWindow); + vi.resetModules(); + + const module = await import("./create_context.js"); + const sandbox = module.createProxyContext( + module.createContext( + createScriptInfo(), + { script: { name: "create-context-test" }, scriptMetaStr: "" }, + "vitest", + undefined as any, + undefined as any, + new Set() + ) + ); + + expect(sandbox.window).toBe(sandbox); + expect(sandbox.self).toBe(sandbox); + }); + }); + describe.concurrent("split-global materialization", () => { it.concurrent("materializes separate realm and host roots through the eager snapshot", () => { const roots = createSplitRealmRoots(); From f0c7c95da61269d0167a8384ab41a62084712524 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Sun, 30 Aug 2026 22:47:17 +0900 Subject: [PATCH 04/12] fix --- .../service/content/create_context.test.ts | 17 ++++++--- src/app/service/content/create_context.ts | 35 +++++++++++++++++-- 2 files changed, 46 insertions(+), 6 deletions(-) diff --git a/src/app/service/content/create_context.test.ts b/src/app/service/content/create_context.test.ts index f1798b01d..26f6cffda 100644 --- a/src/app/service/content/create_context.test.ts +++ b/src/app/service/content/create_context.test.ts @@ -288,7 +288,16 @@ describe.concurrent("createProxyContext", () => { // Firefox 的 content / USER_SCRIPT world 全局是 Cu.Sandbox:globalThis 与 window 分属两个 realm, // 沙盒的原型链在 Xray window 处截断,EventTarget.prototype 上的成员只能经 window 取得。 // happy-dom 里 globalThis === window,只能用一个「仅存在于 window 原型链上」的成员模拟该拓扑。 - describe("Firefox content world:globalThis 与 window 分属不同 realm", () => { + describe.sequential("Firefox content world:globalThis 与 window 分属不同 realm", () => { + const createFakeWindow = (prototype: object | null) => { + const eventTarget = new EventTarget(); + return Object.assign(Object.create(prototype), { + addEventListener: eventTarget.addEventListener.bind(eventTarget), + removeEventListener: eventTarget.removeEventListener.bind(eventTarget), + dispatchEvent: eventTarget.dispatchEvent.bind(eventTarget), + }); + }; + afterEach(() => { vi.unstubAllGlobals(); vi.resetModules(); @@ -302,7 +311,7 @@ describe.concurrent("createProxyContext", () => { return this; }, }.onlyReachableViaWindow; - const fakeWindow = Object.create(windowProto); + const fakeWindow = createFakeWindow(windowProto); vi.stubGlobal("window", fakeWindow); vi.resetModules(); @@ -336,7 +345,7 @@ describe.concurrent("createProxyContext", () => { (FilterLike as any).SHOW_TEXT = 4; windowProto.FilterLike = FilterLike; - const fakeWindow = Object.create(windowProto); + const fakeWindow = createFakeWindow(windowProto); vi.stubGlobal("window", fakeWindow); vi.resetModules(); @@ -361,7 +370,7 @@ describe.concurrent("createProxyContext", () => { // Firefox 下 globalThis.window 是页面 Window 的 Xray 包装,不等于 global; // 只按 global 判定自引用会让沙盒里的 window / self 指回页面, // 脚本写在 self 上的东西(例如沉浸式翻译的 GM_fetch)就落到了页面而不是沙盒。 - const pageWindow: Record = Object.create(null); + const pageWindow: Record = createFakeWindow(null); pageWindow.window = pageWindow; pageWindow.self = pageWindow; vi.stubGlobal("window", pageWindow); diff --git a/src/app/service/content/create_context.ts b/src/app/service/content/create_context.ts index 5e29c16fc..2ec1bd57d 100644 --- a/src/app/service/content/create_context.ts +++ b/src/app/service/content/create_context.ts @@ -193,7 +193,7 @@ const materializeDescriptor = (tracked: TrackedDescriptor): PropertyDescriptor = }; // Firefox 的 content / USER_SCRIPT world 将 JavaScript global 与页面 window 拆成两个 realm。 -// 这里仅转发确定需要页面 brand 的成员;不遍历第二个根,避免把页面全局快照带入沙盒。 +// 这里只读取 hostWindow 的原型链,并转发确定需要页面 brand 的成员,避免把页面全局 own properties 带入沙盒。 const hostWindowAccessors = [ "document", "location", @@ -337,9 +337,40 @@ const createGlobalSnapshot = ({ realmGlobal, hostWindow }: RealmRoots): GlobalSn } } }); + const hostEventKeys = new Set(); + const hostWindowPrototype = Object.getPrototypeOf(hostWindow); + if (hostWindowPrototype) { + getAllPropertyDescriptors(hostWindowPrototype, ([key, desc]) => { + if (!desc || descsCache.has(key) || typeof key !== "string") return; + + if (desc.configurable && desc.get && desc.set && key.startsWith("on")) { + eventDescs[key] = desc; + hostEventKeys.add(key); + return; + } + + const tracked = { + descriptor: desc, + receiver: hostWindow, + isConstructor: isConstructorOrInterface(desc.value), + }; + + if (desc.writable) { + if (shouldFnBind(desc.value)) { + overridedDescs[key] = materializeDescriptor(tracked); + descsCache.add(key); + } else if (!(key in initOwnDescs) && !Object.hasOwn(realmGlobal, key) && !protoBaseDescs[key]) { + protoBaseDescs[key] = materializeDescriptor(tracked); + } + } else if (desc.get || desc.set) { + overridedDescs[key] = materializeDescriptor(tracked); + descsCache.add(key); + } + }); + } + descsCache.clear(); // 内存释放 - const hostEventKeys = new Set(); getAllPropertyDescriptors(hostWindow, ([key, desc]) => { if ( typeof key !== "string" || From 970c3e2ea92a7e76d6c55d726505a591b0009c3b Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Mon, 31 Aug 2026 06:50:32 +0900 Subject: [PATCH 05/12] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20=E9=87=8D=E6=A7=8B?= =?UTF-8?q?=20content=20sandbox=20context?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/service/content/create_context.ts | 177 ++++++++++------------ 1 file changed, 84 insertions(+), 93 deletions(-) diff --git a/src/app/service/content/create_context.ts b/src/app/service/content/create_context.ts index 2ec1bd57d..6a9454bf2 100644 --- a/src/app/service/content/create_context.ts +++ b/src/app/service/content/create_context.ts @@ -2,8 +2,7 @@ import type { TScriptInfo } from "@App/app/repo/scripts"; import { uuidv4 } from "@App/pkg/utils/uuid"; import type { Message } from "@Packages/message/types"; import EventEmitter from "eventemitter3"; -import { GMContextApiGet } from "./gm_api/gm_context"; -import { protect } from "./gm_api/gm_context"; +import { GMContextApiGet, protect } from "./gm_api/gm_context"; import { isEarlyStartScript } from "./utils"; import { ListenerManager } from "./listener_manager"; import { createGMBase } from "./gm_api/gm_api"; @@ -164,6 +163,8 @@ type TrackedDescriptor = { isConstructor: boolean; }; +type DescriptorMap = Record; + const getAllPropertyDescriptors = (obj: DescriptorOwner, callback: (value: DescriptorEntry) => void) => { while (obj && obj !== Object) { const descs = Object.getOwnPropertyDescriptors(obj); @@ -176,8 +177,13 @@ const getAllPropertyDescriptors = (obj: DescriptorOwner, callback: (value: Descr const isConstructorOrInterface = (value: unknown): value is Function => typeof value === "function" && ("prototype" in value || /^[A-Z]/.test(String(Reflect.get(value, "name")))); -const materializeDescriptor = (tracked: TrackedDescriptor): PropertyDescriptor => { - const { descriptor, receiver, isConstructor } = tracked; +const trackDescriptor = (descriptor: PropertyDescriptor, receiver: DescriptorOwner): TrackedDescriptor => ({ + descriptor, + receiver, + isConstructor: isConstructorOrInterface(descriptor.value), +}); + +const materializeDescriptor = ({ descriptor, receiver, isConstructor }: TrackedDescriptor): PropertyDescriptor => { if ("value" in descriptor) { if (typeof descriptor.value !== "function" || isConstructor) return { ...descriptor }; return { @@ -194,7 +200,7 @@ const materializeDescriptor = (tracked: TrackedDescriptor): PropertyDescriptor = // Firefox 的 content / USER_SCRIPT world 将 JavaScript global 与页面 window 拆成两个 realm。 // 这里只读取 hostWindow 的原型链,并转发确定需要页面 brand 的成员,避免把页面全局 own properties 带入沙盒。 -const hostWindowAccessors = [ +const hostWindowAccessors = new Set([ "document", "location", "navigator", @@ -210,8 +216,8 @@ const hostWindowAccessors = [ "scrollX", "scrollY", "devicePixelRatio", -]; -const hostWindowMethods = [ +]); +const hostWindowMethods = new Set([ "addEventListener", "removeEventListener", "dispatchEvent", @@ -223,8 +229,8 @@ const hostWindowMethods = [ "scrollTo", "scrollBy", "blur", -]; -const hostWindowConstructors = [ +]); +const hostWindowConstructors = new Set([ "Window", "EventTarget", "Node", @@ -264,12 +270,12 @@ const hostWindowConstructors = [ "Request", "Response", "XMLHttpRequest", -]; +]); const hostWindowKeys = new Set([...hostWindowAccessors, ...hostWindowMethods, ...hostWindowConstructors]); type GlobalSnapshot = { sharedInitCopy: typeof globalThis & Record; - eventDescs: Record; + eventKeys: Set; }; export type RealmRoots = { @@ -280,9 +286,10 @@ export type RealmRoots = { const createGlobalSnapshot = ({ realmGlobal, hostWindow }: RealmRoots): GlobalSnapshot => { const descsCache: Set = new Set(["eval", "window", "self", "globalThis", "top", "parent"]); const initOwnDescs = Object.getOwnPropertyDescriptors(realmGlobal); - const overridedDescs: Record = Object.create(null); - const eventDescs: Record = Object.create(null); - const protoBaseDescs: Record = Object.create(null); + const overriddenDescs: DescriptorMap = Object.create(null); + const eventKeys = new Set(); + const hostEventKeys = new Set(); + const protoBaseDescs: DescriptorMap = Object.create(null); const getHostWindowDescriptor = (key: string): PropertyDescriptor | undefined => { let owner: DescriptorOwner | null = hostWindow; @@ -294,103 +301,82 @@ const createGlobalSnapshot = ({ realmGlobal, hostWindow }: RealmRoots): GlobalSn return undefined; }; - // 包含物件本身及所有父类(不包含Object)的PropertyDescriptor - // 主要是找出哪些 function值, setter/getter 需要替换 global window - getAllPropertyDescriptors(realmGlobal, ([key, desc]) => { - if (!desc || descsCache.has(key) || typeof key !== "string") return; - - const tracked = { - descriptor: desc, - receiver: realmGlobal, - isConstructor: isConstructorOrInterface(desc.value), - }; - - if (desc.writable) { - // 属性 value - - const value = desc.value; + const collectRealmDescriptors = () => { + // 包含物件本身及所有父类(不包含Object)的PropertyDescriptor。 + // 主要是找出哪些 function 值、setter/getter 需要替换 global window。 + getAllPropertyDescriptors(realmGlobal, ([key, desc]) => { + if (!desc || descsCache.has(key) || typeof key !== "string") return; - // 替换 function 的 this 为 实际的 global window - // 例:父类的 addEventListener - // 对于构造函数和类(有 prototype 属性),shouldFnBind 会返回 false,跳过绑定 - // 因此被封装的属性,会略过封装层,继续向父类寻找原生属性 - if (shouldFnBind(value)) { - overridedDescs[key] = materializeDescriptor(tracked); - descsCache.add(key); // 必须:子类属性覆盖父类属性 - } else if (!(key in initOwnDescs) && !Object.hasOwn(realmGlobal, key)) { - if (!protoBaseDescs[key]) { - protoBaseDescs[key] = materializeDescriptor(tracked); + if (desc.writable) { + // 替换 function 的 this 为实际的 global window。被封装的属性会继续向父类寻找原生属性。 + if (shouldFnBind(desc.value)) { + overriddenDescs[key] = materializeDescriptor(trackDescriptor(desc, realmGlobal)); + descsCache.add(key); // 必须:子类属性覆盖父类属性 + } else if (!(key in initOwnDescs) && !Object.hasOwn(realmGlobal, key) && !protoBaseDescs[key]) { + protoBaseDescs[key] = materializeDescriptor(trackDescriptor(desc, realmGlobal)); } + return; } - } 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] = materializeDescriptor(tracked); - descsCache.add(key); // 必须:子类属性覆盖父类属性 - } + // 替换 onxxxxx 事件赋值操作,例如 (window.)onload、(window.)onerror。 + eventKeys.add(key); + } else if (desc.get || desc.set) { + // 替换 getter/setter 的 this 为实际的 global window,例如 (window.)location、(window.)document。 + overriddenDescs[key] = materializeDescriptor(trackDescriptor(desc, realmGlobal)); + descsCache.add(key); // 必须:子类属性覆盖父类属性 } - } - }); - const hostEventKeys = new Set(); - const hostWindowPrototype = Object.getPrototypeOf(hostWindow); - if (hostWindowPrototype) { + }); + }; + + const collectHostWindowPrototypeDescriptors = () => { + const hostWindowPrototype = Object.getPrototypeOf(hostWindow); + if (!hostWindowPrototype) return; + getAllPropertyDescriptors(hostWindowPrototype, ([key, desc]) => { if (!desc || descsCache.has(key) || typeof key !== "string") return; if (desc.configurable && desc.get && desc.set && key.startsWith("on")) { - eventDescs[key] = desc; + eventKeys.add(key); hostEventKeys.add(key); return; } - const tracked = { - descriptor: desc, - receiver: hostWindow, - isConstructor: isConstructorOrInterface(desc.value), - }; - if (desc.writable) { if (shouldFnBind(desc.value)) { - overridedDescs[key] = materializeDescriptor(tracked); + overriddenDescs[key] = materializeDescriptor(trackDescriptor(desc, hostWindow)); descsCache.add(key); } else if (!(key in initOwnDescs) && !Object.hasOwn(realmGlobal, key) && !protoBaseDescs[key]) { - protoBaseDescs[key] = materializeDescriptor(tracked); + protoBaseDescs[key] = materializeDescriptor(trackDescriptor(desc, hostWindow)); } } else if (desc.get || desc.set) { - overridedDescs[key] = materializeDescriptor(tracked); + overriddenDescs[key] = materializeDescriptor(trackDescriptor(desc, hostWindow)); descsCache.add(key); } }); - } - - descsCache.clear(); // 内存释放 + }; - getAllPropertyDescriptors(hostWindow, ([key, desc]) => { - if ( - typeof key !== "string" || - hostEventKeys.has(key) || - !key.startsWith("on") || - !desc.configurable || - !desc.get || - !desc.set - ) { - return; - } - eventDescs[key] = desc; - hostEventKeys.add(key); - }); + const collectHostWindowEventDescriptors = () => { + getAllPropertyDescriptors(hostWindow, ([key, desc]) => { + if ( + typeof key !== "string" || + hostEventKeys.has(key) || + !key.startsWith("on") || + !desc.configurable || + !desc.get || + !desc.set + ) { + return; + } + eventKeys.add(key); + }); + }; const addHostWindowForwarding = (key: string) => { if (!(key in hostWindow)) return; const hostDescriptor = getHostWindowDescriptor(key); - if (hostWindowAccessors.includes(key)) { - overridedDescs[key] = { + if (hostWindowAccessors.has(key)) { + overriddenDescs[key] = { configurable: true, enumerable: true, get: () => Reflect.get(hostWindow, key, hostWindow), @@ -402,16 +388,16 @@ const createGlobalSnapshot = ({ realmGlobal, hostWindow }: RealmRoots): GlobalSn } : {}), }; - } else if (hostWindowMethods.includes(key)) { + } else if (hostWindowMethods.has(key)) { const method = Reflect.get(hostWindow, key, hostWindow); - overridedDescs[key] = { + overriddenDescs[key] = { configurable: true, enumerable: true, writable: true, value: typeof method === "function" ? Function.prototype.bind.call(method, hostWindow) : method, }; - } else { - overridedDescs[key] = { + } else if (hostWindowConstructors.has(key)) { + overriddenDescs[key] = { configurable: true, enumerable: true, writable: true, @@ -420,6 +406,11 @@ const createGlobalSnapshot = ({ realmGlobal, hostWindow }: RealmRoots): GlobalSn } }; + collectRealmDescriptors(); + collectHostWindowPrototypeDescriptors(); + descsCache.clear(); // 内存释放 + collectHostWindowEventDescriptors(); + for (const key of hostWindowKeys) addHostWindowForwarding(key); // sharedInitCopy: 完全继承Window.prototype 及 自定义 OwnPropertyDescriptor @@ -457,14 +448,14 @@ const createGlobalSnapshot = ({ realmGlobal, hostWindow }: RealmRoots): GlobalSn ...protoBaseDescs, // 较快的 @unwrap 注入时有机会改变 EventTarget.prototype ...Object.getOwnPropertyDescriptors(PseudoWindowPrototype), ...initOwnDescs, - ...overridedDescs, + ...overriddenDescs, }) : Object.create(Object.getPrototypeOf(realmGlobal), { ...initOwnDescs, - ...overridedDescs, + ...overriddenDescs, }); - return { sharedInitCopy, eventDescs }; + return { sharedInitCopy, eventKeys }; }; const defaultGlobalSnapshot = createGlobalSnapshot({ realmGlobal: global, hostWindow: window }); @@ -485,7 +476,7 @@ export const createProxyContext = ( // let withContext: Context | undefined | { [key: string]: any } = undefined; // 为避免做成混乱。 ScriptCat脚本中 self, globalThis, parent 为固定值不能修改 - const { sharedInitCopy, eventDescs } = + const { sharedInitCopy, eventKeys } = roots.realmGlobal === global && roots.hostWindow === window ? defaultGlobalSnapshot : createGlobalSnapshot(roots); const ownDescs = Object.getOwnPropertyDescriptors(sharedInitCopy); @@ -539,7 +530,7 @@ export const createProxyContext = ( }; }; - for (const key of Object.keys(eventDescs)) { + for (const key of eventKeys) { const eventSetterGetter = createEventProp(key); ownDescs[key] = { ...ownDescs[key], From 33c4a3886092cda4552e9ef3748002e85ae82afd Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Mon, 31 Aug 2026 07:03:08 +0900 Subject: [PATCH 06/12] =?UTF-8?q?=F0=9F=93=9D=20=E8=A3=9C=E5=9B=9E=20sandb?= =?UTF-8?q?ox=20context=20=E7=B6=AD=E8=AD=B7=E8=A8=BB=E9=87=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/service/content/create_context.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/app/service/content/create_context.ts b/src/app/service/content/create_context.ts index 6a9454bf2..424ad80a1 100644 --- a/src/app/service/content/create_context.ts +++ b/src/app/service/content/create_context.ts @@ -173,6 +173,7 @@ const getAllPropertyDescriptors = (obj: DescriptorOwner, callback: (value: Descr } }; +// constructor/interface 不可绑定,否则 bind 会丢失 prototype 和静态成员。 // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type const isConstructorOrInterface = (value: unknown): value is Function => typeof value === "function" && ("prototype" in value || /^[A-Z]/.test(String(Reflect.get(value, "name")))); @@ -284,10 +285,13 @@ export type RealmRoots = { }; const createGlobalSnapshot = ({ realmGlobal, hostWindow }: RealmRoots): GlobalSnapshot => { + // descsCache 记录已处理的属性;先处理的 realm/子类 descriptor 不会被后续父类覆盖。 const descsCache: Set = new Set(["eval", "window", "self", "globalThis", "top", "parent"]); const initOwnDescs = Object.getOwnPropertyDescriptors(realmGlobal); + // overriddenDescs 会以 sandbox own descriptor 覆盖原有定义;eventKeys 只记录需要模拟的 on* 属性。 const overriddenDescs: DescriptorMap = Object.create(null); const eventKeys = new Set(); + // hostEventKeys 用于区分 host 原型事件,避免第二次遍历 hostWindow 时重复处理。 const hostEventKeys = new Set(); const protoBaseDescs: DescriptorMap = Object.create(null); @@ -308,7 +312,7 @@ const createGlobalSnapshot = ({ realmGlobal, hostWindow }: RealmRoots): GlobalSn if (!desc || descsCache.has(key) || typeof key !== "string") return; if (desc.writable) { - // 替换 function 的 this 为实际的 global window。被封装的属性会继续向父类寻找原生属性。 + // 替换 function 的 this 为实际的 global window。被封装的属性会继续向父类寻找原生属性;constructor/interface 则保留原值。 if (shouldFnBind(desc.value)) { overriddenDescs[key] = materializeDescriptor(trackDescriptor(desc, realmGlobal)); descsCache.add(key); // 必须:子类属性覆盖父类属性 @@ -406,7 +410,10 @@ const createGlobalSnapshot = ({ realmGlobal, hostWindow }: RealmRoots): GlobalSn } }; + // 第一趟 realmGlobal:Firefox 的 content / USER_SCRIPT world 在这里保留完整的 JavaScript 内置对象。 collectRealmDescriptors(); + // 第二趟 hostWindow 原型链:补齐 Xray window 无法取得的 DOM/EventTarget 成员。 + // 一般的 hostWindow own properties 不应带入沙盒,只有事件属性和下面的白名单会被转发。 collectHostWindowPrototypeDescriptors(); descsCache.clear(); // 内存释放 collectHostWindowEventDescriptors(); @@ -418,6 +425,7 @@ const createGlobalSnapshot = ({ realmGlobal, hostWindow }: RealmRoots): GlobalSn // + 覆盖定义 (document, location, setTimeout, setInterval, addEventListener 等) // sharedInitCopy: ScriptCat脚本共通使用 + // PseudoWindow 没有真实 Window.prototype,因此祖先成员必须先手动复制到 sandbox own descriptors。 const USE_PSEUDO_WINDOW = true; // 日后或能设置使 ScriptCat的沙盒 window 能以 name / id 存取页面元素 class PseudoWindow {} @@ -538,6 +546,7 @@ export const createProxyContext = ( }; } + // split realm 下 hostWindow 可能经由 realmGlobal.window 暴露;这些别名必须始终留在当前 sandbox 内。 for (const key of ["window", "self", "globalThis", "top", "parent", "frames"]) { ownDescs[key] = { configurable: true, From f5b0587a344a06632fa9259c4e917ee5d61686d8 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Mon, 31 Aug 2026 07:26:25 +0900 Subject: [PATCH 07/12] =?UTF-8?q?=E2=9C=85=20=E8=A3=9C=E9=BD=8A=20Firefox?= =?UTF-8?q?=20sandbox=20realm=20regression=20=E6=B8=AC=E8=A9=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/content/create_context.test.ts | 125 +++++++++++++++++- src/app/service/content/create_context.ts | 29 +++- 2 files changed, 149 insertions(+), 5 deletions(-) diff --git a/src/app/service/content/create_context.test.ts b/src/app/service/content/create_context.test.ts index 26f6cffda..b6a9e1461 100644 --- a/src/app/service/content/create_context.test.ts +++ b/src/app/service/content/create_context.test.ts @@ -37,6 +37,10 @@ const createSplitRealmRoots = (): RealmRoots => { const hostWindow = Object.create(null) as Record; const eventTarget = new EventTarget(); + // Firefox USER_SCRIPT 的 realm global 會以 host window 作為原型;測試必須保留這個拓撲, + // 才能確認 realm descriptor 收集不會意外把 host own properties 帶入 sandbox。 + Object.setPrototypeOf(realmGlobal, hostWindow); + realmGlobal.Node = class RealmNode {}; realmGlobal.XMLHttpRequest = class RealmXMLHttpRequest {}; realmGlobal.realmOnly = "realm-value"; @@ -52,6 +56,7 @@ const createSplitRealmRoots = (): RealmRoots => { hostWindow.EventTarget = EventTarget; hostWindow.Node = Node; hostWindow.NodeFilter = NodeFilter; + hostWindow.HTMLBodyElement = class HostHTMLBodyElement {}; hostWindow.Event = Event; hostWindow.XMLHttpRequest = class HostXMLHttpRequest { static DONE = 4; @@ -394,12 +399,59 @@ describe.concurrent("createProxyContext", () => { }); describe.concurrent("split-global materialization", () => { + it.concurrent("keeps GM APIs and writes on every global alias in the script sandbox", () => { + const roots = createSplitRealmRoots(); + const sandbox = createProxyContext(createTestContext(["GM_getValue"]), roots); + + const getValue = Reflect.get(sandbox.window, "GM_getValue") as (key: string) => unknown; + expect(getValue("foo")).toBe("bar"); + + Reflect.set(sandbox.self, "__split_global_alias_value", "sandbox-value"); + + expect(Reflect.get(sandbox.window, "__split_global_alias_value")).toBe("sandbox-value"); + expect(Reflect.get(sandbox.globalThis, "__split_global_alias_value")).toBe("sandbox-value"); + expect(Reflect.get(roots.hostWindow, "__split_global_alias_value")).toBeUndefined(); + }); + + it.concurrent("keeps JavaScript intrinsics from realmGlobal when hostWindow is a different root", () => { + const roots = createSplitRealmRoots(); + const realmMath = { max: () => "realm" }; + const hostMath = { max: () => "host" }; + roots.realmGlobal.Math = realmMath; + roots.hostWindow.Math = hostMath; + + const sandbox = createProxyContext(createTestContext([]), roots); + + expect(sandbox.Math).toBe(realmMath); + expect(sandbox.Math).not.toBe(hostMath); + }); + + it.concurrent("preserves non-self top, parent, and frames references from an iframe realm", () => { + const roots = createSplitRealmRoots(); + const parentWindow = Object.create(null); + const topWindow = Object.create(null); + const frames = Object.create(null); + roots.hostWindow.parent = parentWindow; + roots.hostWindow.top = topWindow; + roots.hostWindow.frames = frames; + + const sandbox = createProxyContext(createTestContext([]), roots); + + expect(sandbox.parent).toBe(parentWindow); + expect(sandbox.top).toBe(topWindow); + expect(sandbox.frames).toBe(frames); + }); + it.concurrent("materializes separate realm and host roots through the eager snapshot", () => { const roots = createSplitRealmRoots(); const sandbox = createProxyContext(createTestContext([]), roots); expect(sandbox.Node).toBe(roots.hostWindow.Node); expect(sandbox.Node.prototype).toBe(roots.hostWindow.Node.prototype); + expect(sandbox.NodeFilter).toBe(roots.hostWindow.NodeFilter); + expect(sandbox.NodeFilter.SHOW_TEXT).toBe(roots.hostWindow.NodeFilter.SHOW_TEXT); + expect(sandbox.HTMLBodyElement).toBe(roots.hostWindow.HTMLBodyElement); + expect(sandbox.HTMLBodyElement.prototype).toBe(roots.hostWindow.HTMLBodyElement.prototype); expect(sandbox.XMLHttpRequest).toBe(roots.hostWindow.XMLHttpRequest); expect(sandbox.XMLHttpRequest.DONE).toBe(4); expect(sandbox.EventTarget).toBe(roots.hostWindow.EventTarget); @@ -429,7 +481,11 @@ describe.concurrent("createProxyContext", () => { }); it.concurrent("keeps all self-referential window names inside the sandbox", () => { - const sandbox = createProxyContext(createTestContext([])); + const roots = createSplitRealmRoots(); + roots.hostWindow.top = roots.hostWindow; + roots.hostWindow.parent = roots.hostWindow; + roots.hostWindow.frames = roots.hostWindow; + const sandbox = createProxyContext(createTestContext([]), roots); expect(sandbox.window).toBe(sandbox); expect(sandbox.self).toBe(sandbox); @@ -509,6 +565,28 @@ describe.concurrent("createProxyContext", () => { expect(handler).toHaveBeenCalledTimes(1); }); + it.concurrent("removes a function listener before storing an object handler, then accepts a new function", () => { + const sandbox = createProxyContext(createTestContext([])); + const oldHandler = vi.fn(); + const objectHandler = { handleEvent: vi.fn() }; + const newHandler = vi.fn(); + + try { + sandbox.onblur = oldHandler; + Reflect.set(sandbox, "onblur", objectHandler); + window.dispatchEvent(new Event("blur")); + + expect(oldHandler).not.toHaveBeenCalled(); + expect(objectHandler.handleEvent).not.toHaveBeenCalled(); + + sandbox.onblur = newHandler; + window.dispatchEvent(new Event("blur")); + expect(newHandler).toHaveBeenCalledTimes(1); + } finally { + sandbox.onblur = null; + } + }); + it.concurrent("replaces an on-property listener without retaining the previous callback", () => { const sandbox = createProxyContext(createTestContext([])); const oldHandler = vi.fn(); @@ -543,5 +621,50 @@ describe.concurrent("createProxyContext", () => { expect(sandbox).not.toBe(window); expect(sandbox.unsafeWindow).toBe(window); }); + + it.concurrent( + "uses hostWindow as the receiver for host prototype accessors and keeps the nearest descriptor", + () => { + const roots = createSplitRealmRoots(); + const parentPrototype = Object.create(null); + const hostPrototype = Object.create(parentPrototype); + let hostValue = "unset"; + + Object.defineProperty(parentPrototype, "precedenceAccessor", { + configurable: true, + enumerable: true, + get: () => "parent", + set: () => undefined, + }); + Object.defineProperty(hostPrototype, "precedenceAccessor", { + configurable: true, + enumerable: true, + get() { + return this === roots.hostWindow ? "host" : "wrong-receiver"; + }, + set(value: string) { + hostValue = this === roots.hostWindow ? value : "wrong-receiver"; + }, + }); + Object.defineProperty(hostPrototype, "hostAccessor", { + configurable: true, + enumerable: true, + get() { + return this === roots.hostWindow ? "host" : "wrong-receiver"; + }, + set(value: string) { + hostValue = this === roots.hostWindow ? value : "wrong-receiver"; + }, + }); + Object.setPrototypeOf(roots.hostWindow, hostPrototype); + + const sandbox = createProxyContext(createTestContext([]), roots); + + expect(sandbox.precedenceAccessor).toBe("host"); + expect(sandbox.hostAccessor).toBe("host"); + sandbox.hostAccessor = "updated"; + expect(hostValue).toBe("updated"); + } + ); }); }); diff --git a/src/app/service/content/create_context.ts b/src/app/service/content/create_context.ts index 424ad80a1..a7e35644d 100644 --- a/src/app/service/content/create_context.ts +++ b/src/app/service/content/create_context.ts @@ -237,6 +237,7 @@ const hostWindowConstructors = new Set([ "Node", "Element", "HTMLElement", + "HTMLBodyElement", "Document", "DocumentFragment", "ShadowRoot", @@ -306,9 +307,11 @@ const createGlobalSnapshot = ({ realmGlobal, hostWindow }: RealmRoots): GlobalSn }; const collectRealmDescriptors = () => { - // 包含物件本身及所有父类(不包含Object)的PropertyDescriptor。 - // 主要是找出哪些 function 值、setter/getter 需要替换 global window。 - getAllPropertyDescriptors(realmGlobal, ([key, desc]) => { + // 只读取 realmGlobal own descriptors,避免沿 Firefox 的 hostWindow 原型链混合两个 realm。 + // 主要是找出哪些 function 值、setter/getter 需要替换 host window。 + const descriptors = Object.getOwnPropertyDescriptors(realmGlobal); + Reflect.ownKeys(descriptors).forEach((key) => { + const desc = descriptors[key as keyof typeof descriptors]; if (!desc || descsCache.has(key) || typeof key !== "string") return; if (desc.writable) { @@ -547,7 +550,7 @@ export const createProxyContext = ( } // split realm 下 hostWindow 可能经由 realmGlobal.window 暴露;这些别名必须始终留在当前 sandbox 内。 - for (const key of ["window", "self", "globalThis", "top", "parent", "frames"]) { + for (const key of ["window", "self", "globalThis"]) { ownDescs[key] = { configurable: true, enumerable: true, @@ -556,6 +559,24 @@ export const createProxyContext = ( }, }; } + for (const key of ["top", "parent", "frames"]) { + const descriptor = ownDescs[key]; + const hostValue = Reflect.get(roots.hostWindow, key, roots.hostWindow); + if (hostValue === undefined && !descriptor) continue; + + ownDescs[key] = { + ...descriptor, + configurable: true, + enumerable: descriptor?.enumerable ?? true, + get() { + const value = Reflect.get(roots.hostWindow, key, roots.hostWindow); + return value === roots.hostWindow || value === roots.realmGlobal ? mySandbox : value; + }, + set: undefined, + }; + delete ownDescs[key].value; + delete ownDescs[key].writable; + } if (noEval) { if (ownDescs?.eval?.value) { ownDescs.eval.value = undefined; From 5721a6b8316b3e00adf5e056d0dfb9e01a81c8b3 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Mon, 31 Aug 2026 07:30:23 +0900 Subject: [PATCH 08/12] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20=E7=B0=A1=E5=8C=96?= =?UTF-8?q?=20sandbox=20descriptor=20materialization?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/service/content/create_context.ts | 49 ++++++++++------------- 1 file changed, 22 insertions(+), 27 deletions(-) diff --git a/src/app/service/content/create_context.ts b/src/app/service/content/create_context.ts index a7e35644d..af47745f4 100644 --- a/src/app/service/content/create_context.ts +++ b/src/app/service/content/create_context.ts @@ -156,42 +156,37 @@ export const shouldFnBind = (f: any) => { // 取物件本身及所有父类(不包含Object)的PropertyDescriptor type DescriptorOwner = Record; -type DescriptorEntry = [string | symbol, PropertyDescriptor]; -type TrackedDescriptor = { - descriptor: PropertyDescriptor; - receiver: DescriptorOwner; - isConstructor: boolean; -}; type DescriptorMap = Record; -const getAllPropertyDescriptors = (obj: DescriptorOwner, callback: (value: DescriptorEntry) => void) => { +const getAllPropertyDescriptors = ( + obj: DescriptorOwner, + callback: (key: string | symbol, descriptor: PropertyDescriptor) => void +) => { while (obj && obj !== Object) { const descs = Object.getOwnPropertyDescriptors(obj); - Reflect.ownKeys(descs).forEach((key) => callback([key, descs[key as keyof typeof descs]])); + Reflect.ownKeys(descs).forEach((key) => callback(key, descs[key as keyof typeof descs])); obj = Object.getPrototypeOf(obj); } }; // constructor/interface 不可绑定,否则 bind 会丢失 prototype 和静态成员。 -// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type -const isConstructorOrInterface = (value: unknown): value is Function => - typeof value === "function" && ("prototype" in value || /^[A-Z]/.test(String(Reflect.get(value, "name")))); - -const trackDescriptor = (descriptor: PropertyDescriptor, receiver: DescriptorOwner): TrackedDescriptor => ({ - descriptor, - receiver, - isConstructor: isConstructorOrInterface(descriptor.value), -}); +const isConstructorOrInterface = (value: unknown) => { + if (typeof value !== "function") return false; + if ("prototype" in value) return true; + const firstChar = (value as { name: string }).name.charCodeAt(0); + return firstChar >= 65 && firstChar <= 90; +}; -const materializeDescriptor = ({ descriptor, receiver, isConstructor }: TrackedDescriptor): PropertyDescriptor => { +const materializeDescriptor = (descriptor: PropertyDescriptor, receiver: DescriptorOwner): PropertyDescriptor => { if ("value" in descriptor) { - if (typeof descriptor.value !== "function" || isConstructor) return { ...descriptor }; + if (typeof descriptor.value !== "function" || isConstructorOrInterface(descriptor.value)) return descriptor; return { ...descriptor, value: Function.prototype.bind.call(descriptor.value, receiver), }; } + if (!descriptor.get && !descriptor.set) return descriptor; return { ...descriptor, get: descriptor.get?.bind(receiver), @@ -317,10 +312,10 @@ const createGlobalSnapshot = ({ realmGlobal, hostWindow }: RealmRoots): GlobalSn if (desc.writable) { // 替换 function 的 this 为实际的 global window。被封装的属性会继续向父类寻找原生属性;constructor/interface 则保留原值。 if (shouldFnBind(desc.value)) { - overriddenDescs[key] = materializeDescriptor(trackDescriptor(desc, realmGlobal)); + overriddenDescs[key] = materializeDescriptor(desc, realmGlobal); descsCache.add(key); // 必须:子类属性覆盖父类属性 } else if (!(key in initOwnDescs) && !Object.hasOwn(realmGlobal, key) && !protoBaseDescs[key]) { - protoBaseDescs[key] = materializeDescriptor(trackDescriptor(desc, realmGlobal)); + protoBaseDescs[key] = materializeDescriptor(desc, realmGlobal); } return; } @@ -330,7 +325,7 @@ const createGlobalSnapshot = ({ realmGlobal, hostWindow }: RealmRoots): GlobalSn eventKeys.add(key); } else if (desc.get || desc.set) { // 替换 getter/setter 的 this 为实际的 global window,例如 (window.)location、(window.)document。 - overriddenDescs[key] = materializeDescriptor(trackDescriptor(desc, realmGlobal)); + overriddenDescs[key] = materializeDescriptor(desc, realmGlobal); descsCache.add(key); // 必须:子类属性覆盖父类属性 } }); @@ -340,7 +335,7 @@ const createGlobalSnapshot = ({ realmGlobal, hostWindow }: RealmRoots): GlobalSn const hostWindowPrototype = Object.getPrototypeOf(hostWindow); if (!hostWindowPrototype) return; - getAllPropertyDescriptors(hostWindowPrototype, ([key, desc]) => { + getAllPropertyDescriptors(hostWindowPrototype, (key, desc) => { if (!desc || descsCache.has(key) || typeof key !== "string") return; if (desc.configurable && desc.get && desc.set && key.startsWith("on")) { @@ -351,20 +346,20 @@ const createGlobalSnapshot = ({ realmGlobal, hostWindow }: RealmRoots): GlobalSn if (desc.writable) { if (shouldFnBind(desc.value)) { - overriddenDescs[key] = materializeDescriptor(trackDescriptor(desc, hostWindow)); + overriddenDescs[key] = materializeDescriptor(desc, hostWindow); descsCache.add(key); } else if (!(key in initOwnDescs) && !Object.hasOwn(realmGlobal, key) && !protoBaseDescs[key]) { - protoBaseDescs[key] = materializeDescriptor(trackDescriptor(desc, hostWindow)); + protoBaseDescs[key] = materializeDescriptor(desc, hostWindow); } } else if (desc.get || desc.set) { - overriddenDescs[key] = materializeDescriptor(trackDescriptor(desc, hostWindow)); + overriddenDescs[key] = materializeDescriptor(desc, hostWindow); descsCache.add(key); } }); }; const collectHostWindowEventDescriptors = () => { - getAllPropertyDescriptors(hostWindow, ([key, desc]) => { + getAllPropertyDescriptors(hostWindow, (key, desc) => { if ( typeof key !== "string" || hostEventKeys.has(key) || From a4fc4efc713ae88dad718b47277d2191438ff705 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Mon, 31 Aug 2026 21:12:47 +0900 Subject: [PATCH 09/12] Update create_context.ts --- src/app/service/content/create_context.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/app/service/content/create_context.ts b/src/app/service/content/create_context.ts index af47745f4..55fd5e6ad 100644 --- a/src/app/service/content/create_context.ts +++ b/src/app/service/content/create_context.ts @@ -165,7 +165,9 @@ const getAllPropertyDescriptors = ( ) => { while (obj && obj !== Object) { const descs = Object.getOwnPropertyDescriptors(obj); - Reflect.ownKeys(descs).forEach((key) => callback(key, descs[key as keyof typeof descs])); + for (const key of Reflect.ownKeys(descs)) { + callback(key, descs[key as keyof typeof descs]); + } obj = Object.getPrototypeOf(obj); } }; @@ -305,9 +307,9 @@ const createGlobalSnapshot = ({ realmGlobal, hostWindow }: RealmRoots): GlobalSn // 只读取 realmGlobal own descriptors,避免沿 Firefox 的 hostWindow 原型链混合两个 realm。 // 主要是找出哪些 function 值、setter/getter 需要替换 host window。 const descriptors = Object.getOwnPropertyDescriptors(realmGlobal); - Reflect.ownKeys(descriptors).forEach((key) => { + for (const key of Reflect.ownKeys(descriptors)) { const desc = descriptors[key as keyof typeof descriptors]; - if (!desc || descsCache.has(key) || typeof key !== "string") return; + if (!desc || descsCache.has(key) || typeof key !== "string") continue; if (desc.writable) { // 替换 function 的 this 为实际的 global window。被封装的属性会继续向父类寻找原生属性;constructor/interface 则保留原值。 @@ -317,7 +319,7 @@ const createGlobalSnapshot = ({ realmGlobal, hostWindow }: RealmRoots): GlobalSn } else if (!(key in initOwnDescs) && !Object.hasOwn(realmGlobal, key) && !protoBaseDescs[key]) { protoBaseDescs[key] = materializeDescriptor(desc, realmGlobal); } - return; + continue; } if (desc.configurable && desc.get && desc.set && desc.enumerable && key.startsWith("on")) { @@ -328,7 +330,7 @@ const createGlobalSnapshot = ({ realmGlobal, hostWindow }: RealmRoots): GlobalSn overriddenDescs[key] = materializeDescriptor(desc, realmGlobal); descsCache.add(key); // 必须:子类属性覆盖父类属性 } - }); + } }; const collectHostWindowPrototypeDescriptors = () => { From 8e02a9d0c004fda3c81cc275071df766bcd040c7 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Tue, 1 Sep 2026 06:03:53 +0900 Subject: [PATCH 10/12] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20=E7=B0=A1=E5=8C=96?= =?UTF-8?q?=20sandbox=20descriptor=20=E6=94=B6=E9=9B=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/service/content/create_context.ts | 31 +++++++---------------- 1 file changed, 9 insertions(+), 22 deletions(-) diff --git a/src/app/service/content/create_context.ts b/src/app/service/content/create_context.ts index 55fd5e6ad..cc141563b 100644 --- a/src/app/service/content/create_context.ts +++ b/src/app/service/content/create_context.ts @@ -283,14 +283,12 @@ export type RealmRoots = { }; const createGlobalSnapshot = ({ realmGlobal, hostWindow }: RealmRoots): GlobalSnapshot => { - // descsCache 记录已处理的属性;先处理的 realm/子类 descriptor 不会被后续父类覆盖。 + // descsCache 记录已处理的属性;先处理的 descriptor 覆盖后续父类。 const descsCache: Set = new Set(["eval", "window", "self", "globalThis", "top", "parent"]); const initOwnDescs = Object.getOwnPropertyDescriptors(realmGlobal); // overriddenDescs 会以 sandbox own descriptor 覆盖原有定义;eventKeys 只记录需要模拟的 on* 属性。 const overriddenDescs: DescriptorMap = Object.create(null); const eventKeys = new Set(); - // hostEventKeys 用于区分 host 原型事件,避免第二次遍历 hostWindow 时重复处理。 - const hostEventKeys = new Set(); const protoBaseDescs: DescriptorMap = Object.create(null); const getHostWindowDescriptor = (key: string): PropertyDescriptor | undefined => { @@ -304,29 +302,26 @@ const createGlobalSnapshot = ({ realmGlobal, hostWindow }: RealmRoots): GlobalSn }; const collectRealmDescriptors = () => { - // 只读取 realmGlobal own descriptors,避免沿 Firefox 的 hostWindow 原型链混合两个 realm。 - // 主要是找出哪些 function 值、setter/getter 需要替换 host window。 + // 只读取 realmGlobal own descriptors,避免混合 Firefox 的两个 realm。 const descriptors = Object.getOwnPropertyDescriptors(realmGlobal); for (const key of Reflect.ownKeys(descriptors)) { const desc = descriptors[key as keyof typeof descriptors]; if (!desc || descsCache.has(key) || typeof key !== "string") continue; if (desc.writable) { - // 替换 function 的 this 为实际的 global window。被封装的属性会继续向父类寻找原生属性;constructor/interface 则保留原值。 + // 原生 function 绑定到所属 root;constructor/interface 保留原值。 if (shouldFnBind(desc.value)) { overriddenDescs[key] = materializeDescriptor(desc, realmGlobal); descsCache.add(key); // 必须:子类属性覆盖父类属性 - } else if (!(key in initOwnDescs) && !Object.hasOwn(realmGlobal, key) && !protoBaseDescs[key]) { - protoBaseDescs[key] = materializeDescriptor(desc, realmGlobal); } continue; } if (desc.configurable && desc.get && desc.set && desc.enumerable && key.startsWith("on")) { - // 替换 onxxxxx 事件赋值操作,例如 (window.)onload、(window.)onerror。 + // 替换 onxxxxx 事件赋值操作。 eventKeys.add(key); } else if (desc.get || desc.set) { - // 替换 getter/setter 的 this 为实际的 global window,例如 (window.)location、(window.)document。 + // 替换 getter/setter 的 this 为实际的 global window。 overriddenDescs[key] = materializeDescriptor(desc, realmGlobal); descsCache.add(key); // 必须:子类属性覆盖父类属性 } @@ -342,7 +337,6 @@ const createGlobalSnapshot = ({ realmGlobal, hostWindow }: RealmRoots): GlobalSn if (desc.configurable && desc.get && desc.set && key.startsWith("on")) { eventKeys.add(key); - hostEventKeys.add(key); return; } @@ -362,14 +356,7 @@ const createGlobalSnapshot = ({ realmGlobal, hostWindow }: RealmRoots): GlobalSn const collectHostWindowEventDescriptors = () => { getAllPropertyDescriptors(hostWindow, (key, desc) => { - if ( - typeof key !== "string" || - hostEventKeys.has(key) || - !key.startsWith("on") || - !desc.configurable || - !desc.get || - !desc.set - ) { + if (typeof key !== "string" || !key.startsWith("on") || !desc.configurable || !desc.get || !desc.set) { return; } eventKeys.add(key); @@ -410,10 +397,10 @@ const createGlobalSnapshot = ({ realmGlobal, hostWindow }: RealmRoots): GlobalSn } }; - // 第一趟 realmGlobal:Firefox 的 content / USER_SCRIPT world 在这里保留完整的 JavaScript 内置对象。 + // 第一趟 realmGlobal:保留 JavaScript 内置对象。 collectRealmDescriptors(); - // 第二趟 hostWindow 原型链:补齐 Xray window 无法取得的 DOM/EventTarget 成员。 - // 一般的 hostWindow own properties 不应带入沙盒,只有事件属性和下面的白名单会被转发。 + // 第二趟 hostWindow 原型链:补齐 Xray 截断的 DOM/EventTarget 成员。 + // hostWindow own properties 仅由事件属性和白名单转发。 collectHostWindowPrototypeDescriptors(); descsCache.clear(); // 内存释放 collectHostWindowEventDescriptors(); From b6ba024cf2d5511b1f83458a28e03723af9c19fa Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Tue, 1 Sep 2026 07:24:49 +0900 Subject: [PATCH 11/12] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20=E6=8B=86=E5=88=86?= =?UTF-8?q?=20realm=20=E4=B8=8E=20host=20descriptor=20=E6=94=B6=E9=9B=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/content/create_context.test.ts | 61 ++++--- src/app/service/content/create_context.ts | 149 +----------------- 2 files changed, 48 insertions(+), 162 deletions(-) diff --git a/src/app/service/content/create_context.test.ts b/src/app/service/content/create_context.test.ts index b6a9e1461..d097046bd 100644 --- a/src/app/service/content/create_context.test.ts +++ b/src/app/service/content/create_context.test.ts @@ -35,14 +35,13 @@ const createTestContext = (grants: string[], metadata: Record const createSplitRealmRoots = (): RealmRoots => { const realmGlobal = Object.create(null) as Record; const hostWindow = Object.create(null) as Record; + const hostWindowPrototype = Object.create(null) as Record; const eventTarget = new EventTarget(); - // Firefox USER_SCRIPT 的 realm global 會以 host window 作為原型;測試必須保留這個拓撲, - // 才能確認 realm descriptor 收集不會意外把 host own properties 帶入 sandbox。 + // Firefox USER_SCRIPT 的 realm global 會以 host window 作為原型。 Object.setPrototypeOf(realmGlobal, hostWindow); + Object.setPrototypeOf(hostWindow, hostWindowPrototype); - realmGlobal.Node = class RealmNode {}; - realmGlobal.XMLHttpRequest = class RealmXMLHttpRequest {}; realmGlobal.realmOnly = "realm-value"; Object.defineProperty(realmGlobal, "realmAccessor", { configurable: true, @@ -62,10 +61,33 @@ const createSplitRealmRoots = (): RealmRoots => { static DONE = 4; }; hostWindow.document = document; - hostWindow.hostOnly = "host-value"; hostWindow.addEventListener = eventTarget.addEventListener.bind(eventTarget); hostWindow.removeEventListener = eventTarget.removeEventListener.bind(eventTarget); hostWindow.dispatchEvent = eventTarget.dispatchEvent.bind(eventTarget); + hostWindow.dynamicHostMethod = { + dynamicHostMethod(this: unknown) { + return this; + }, + }.dynamicHostMethod; + let dynamicHostValue = "host-value"; + Object.defineProperty(hostWindow, "dynamicHostAccessor", { + configurable: true, + enumerable: true, + get() { + return this === hostWindow ? dynamicHostValue : "wrong-receiver"; + }, + set(value) { + dynamicHostValue = value; + }, + }); + const DynamicInterface = function DynamicInterface() {}; + (DynamicInterface as any).staticValue = "static-value"; + hostWindow.DynamicInterface = DynamicInterface; + hostWindowPrototype.dynamicPrototypeMethod = { + dynamicPrototypeMethod(this: unknown) { + return this; + }, + }.dynamicPrototypeMethod; Object.defineProperty(hostWindow, "onload", { configurable: true, enumerable: true, @@ -426,6 +448,20 @@ describe.concurrent("createProxyContext", () => { expect(sandbox.Math).not.toBe(hostMath); }); + it.concurrent("collects dynamic host own and prototype descriptors", () => { + const roots = createSplitRealmRoots(); + const sandbox = createProxyContext(createTestContext([]), roots); + + expect(sandbox.dynamicHostMethod()).toBe(roots.hostWindow); + expect(sandbox.dynamicHostAccessor).toBe("host-value"); + expect(sandbox.dynamicPrototypeMethod()).toBe(roots.hostWindow); + expect(sandbox.DynamicInterface.staticValue).toBe("static-value"); + expect(sandbox.DynamicInterface.prototype).toBe(roots.hostWindow.DynamicInterface.prototype); + + sandbox.dynamicHostAccessor = "updated-value"; + expect(sandbox.dynamicHostAccessor).toBe("updated-value"); + }); + it.concurrent("preserves non-self top, parent, and frames references from an iframe realm", () => { const roots = createSplitRealmRoots(); const parentWindow = Object.create(null); @@ -458,7 +494,6 @@ describe.concurrent("createProxyContext", () => { expect(sandbox.document).toBe(document); expect(sandbox.realmOnly).toBe("realm-value"); expect(sandbox.realmAccessor).toBe("realm-receiver"); - expect(sandbox.hostOnly).toBeUndefined(); const listener = vi.fn(); sandbox.addEventListener("split-root", listener); @@ -495,20 +530,6 @@ describe.concurrent("createProxyContext", () => { expect(sandbox.frames).toBe(sandbox); }); - it.concurrent("forwards live host accessors without leaking page globals", () => { - const sandbox = createProxyContext(createTestContext([])); - const pageKey = "__scriptcat_split_global_page_value"; - - Reflect.set(window, pageKey, "page-value"); - try { - expect(sandbox.document).toBe(window.document); - expect(sandbox.location).toBe(window.location); - expect(sandbox[pageKey]).toBeUndefined(); - } finally { - Reflect.deleteProperty(window, pageKey); - } - }); - it.concurrent("preserves host constructor static constants and prototype identity", () => { const sandbox = createProxyContext(createTestContext([])); diff --git a/src/app/service/content/create_context.ts b/src/app/service/content/create_context.ts index cc141563b..d30ed02e2 100644 --- a/src/app/service/content/create_context.ts +++ b/src/app/service/content/create_context.ts @@ -196,82 +196,6 @@ const materializeDescriptor = (descriptor: PropertyDescriptor, receiver: Descrip }; }; -// Firefox 的 content / USER_SCRIPT world 将 JavaScript global 与页面 window 拆成两个 realm。 -// 这里只读取 hostWindow 的原型链,并转发确定需要页面 brand 的成员,避免把页面全局 own properties 带入沙盒。 -const hostWindowAccessors = new Set([ - "document", - "location", - "navigator", - "history", - "screen", - "performance", - "crypto", - "localStorage", - "sessionStorage", - "visualViewport", - "innerWidth", - "innerHeight", - "scrollX", - "scrollY", - "devicePixelRatio", -]); -const hostWindowMethods = new Set([ - "addEventListener", - "removeEventListener", - "dispatchEvent", - "getComputedStyle", - "matchMedia", - "requestAnimationFrame", - "cancelAnimationFrame", - "scroll", - "scrollTo", - "scrollBy", - "blur", -]); -const hostWindowConstructors = new Set([ - "Window", - "EventTarget", - "Node", - "Element", - "HTMLElement", - "HTMLBodyElement", - "Document", - "DocumentFragment", - "ShadowRoot", - "Text", - "Range", - "MutationObserver", - "NodeFilter", - "TreeWalker", - "Event", - "CustomEvent", - "MouseEvent", - "KeyboardEvent", - "PointerEvent", - "InputEvent", - "FocusEvent", - "ErrorEvent", - "ProgressEvent", - "MessageEvent", - "StorageEvent", - "WheelEvent", - "DragEvent", - "ClipboardEvent", - "DOMParser", - "XMLSerializer", - "FormData", - "File", - "FileList", - "Blob", - "URL", - "URLSearchParams", - "Headers", - "Request", - "Response", - "XMLHttpRequest", -]); -const hostWindowKeys = new Set([...hostWindowAccessors, ...hostWindowMethods, ...hostWindowConstructors]); - type GlobalSnapshot = { sharedInitCopy: typeof globalThis & Record; eventKeys: Set; @@ -291,16 +215,6 @@ const createGlobalSnapshot = ({ realmGlobal, hostWindow }: RealmRoots): GlobalSn const eventKeys = new Set(); const protoBaseDescs: DescriptorMap = Object.create(null); - const getHostWindowDescriptor = (key: string): PropertyDescriptor | undefined => { - let owner: DescriptorOwner | null = hostWindow; - while (owner) { - const descriptor = Object.getOwnPropertyDescriptor(owner, key); - if (descriptor) return descriptor; - owner = Object.getPrototypeOf(owner); - } - return undefined; - }; - const collectRealmDescriptors = () => { // 只读取 realmGlobal own descriptors,避免混合 Firefox 的两个 realm。 const descriptors = Object.getOwnPropertyDescriptors(realmGlobal); @@ -328,19 +242,17 @@ const createGlobalSnapshot = ({ realmGlobal, hostWindow }: RealmRoots): GlobalSn } }; - const collectHostWindowPrototypeDescriptors = () => { - const hostWindowPrototype = Object.getPrototypeOf(hostWindow); - if (!hostWindowPrototype) return; - - getAllPropertyDescriptors(hostWindowPrototype, (key, desc) => { - if (!desc || descsCache.has(key) || typeof key !== "string") return; + const collectHostWindowDescriptors = () => { + getAllPropertyDescriptors(hostWindow, (key, desc) => { + if (!desc || typeof key !== "string") return; if (desc.configurable && desc.get && desc.set && key.startsWith("on")) { eventKeys.add(key); return; } + if (descsCache.has(key)) return; - if (desc.writable) { + if ("value" in desc) { if (shouldFnBind(desc.value)) { overriddenDescs[key] = materializeDescriptor(desc, hostWindow); descsCache.add(key); @@ -354,58 +266,11 @@ const createGlobalSnapshot = ({ realmGlobal, hostWindow }: RealmRoots): GlobalSn }); }; - const collectHostWindowEventDescriptors = () => { - getAllPropertyDescriptors(hostWindow, (key, desc) => { - if (typeof key !== "string" || !key.startsWith("on") || !desc.configurable || !desc.get || !desc.set) { - return; - } - eventKeys.add(key); - }); - }; - - const addHostWindowForwarding = (key: string) => { - if (!(key in hostWindow)) return; - const hostDescriptor = getHostWindowDescriptor(key); - if (hostWindowAccessors.has(key)) { - overriddenDescs[key] = { - configurable: true, - enumerable: true, - get: () => Reflect.get(hostWindow, key, hostWindow), - ...(hostDescriptor?.set - ? { - set: (value: any) => { - Reflect.set(hostWindow, key, value, hostWindow); - }, - } - : {}), - }; - } else if (hostWindowMethods.has(key)) { - const method = Reflect.get(hostWindow, key, hostWindow); - overriddenDescs[key] = { - configurable: true, - enumerable: true, - writable: true, - value: typeof method === "function" ? Function.prototype.bind.call(method, hostWindow) : method, - }; - } else if (hostWindowConstructors.has(key)) { - overriddenDescs[key] = { - configurable: true, - enumerable: true, - writable: true, - value: Reflect.get(hostWindow, key, hostWindow), - }; - } - }; - // 第一趟 realmGlobal:保留 JavaScript 内置对象。 collectRealmDescriptors(); - // 第二趟 hostWindow 原型链:补齐 Xray 截断的 DOM/EventTarget 成员。 - // hostWindow own properties 仅由事件属性和白名单转发。 - collectHostWindowPrototypeDescriptors(); + // 第二趟 hostWindow:补齐 Firefox split-realm 的 host 成员。 + collectHostWindowDescriptors(); descsCache.clear(); // 内存释放 - collectHostWindowEventDescriptors(); - - for (const key of hostWindowKeys) addHostWindowForwarding(key); // sharedInitCopy: 完全继承Window.prototype 及 自定义 OwnPropertyDescriptor // OwnPropertyDescriptor定义 为 原OwnPropertyDescriptor定义 (DragEvent, MouseEvent, RegExp, EventTarget, JSON等) From c9cb643084a035c19661f150a2d50881df8f7fad Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Tue, 1 Sep 2026 08:02:09 +0900 Subject: [PATCH 12/12] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20=E7=B0=A1=E5=8C=96?= =?UTF-8?q?=20realm=20=E8=88=87=20host=20descriptor=20=E6=94=B6=E9=9B=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/service/content/create_context.ts | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/app/service/content/create_context.ts b/src/app/service/content/create_context.ts index d30ed02e2..d2fb32c54 100644 --- a/src/app/service/content/create_context.ts +++ b/src/app/service/content/create_context.ts @@ -218,13 +218,13 @@ const createGlobalSnapshot = ({ realmGlobal, hostWindow }: RealmRoots): GlobalSn const collectRealmDescriptors = () => { // 只读取 realmGlobal own descriptors,避免混合 Firefox 的两个 realm。 const descriptors = Object.getOwnPropertyDescriptors(realmGlobal); - for (const key of Reflect.ownKeys(descriptors)) { - const desc = descriptors[key as keyof typeof descriptors]; - if (!desc || descsCache.has(key) || typeof key !== "string") continue; + for (const key of Object.keys(descriptors)) { + const desc = descriptors[key]; + if (descsCache.has(key)) continue; - if (desc.writable) { + if ("value" in desc) { // 原生 function 绑定到所属 root;constructor/interface 保留原值。 - if (shouldFnBind(desc.value)) { + if (desc.writable && shouldFnBind(desc.value)) { overriddenDescs[key] = materializeDescriptor(desc, realmGlobal); descsCache.add(key); // 必须:子类属性覆盖父类属性 } @@ -234,7 +234,9 @@ const createGlobalSnapshot = ({ realmGlobal, hostWindow }: RealmRoots): GlobalSn if (desc.configurable && desc.get && desc.set && desc.enumerable && key.startsWith("on")) { // 替换 onxxxxx 事件赋值操作。 eventKeys.add(key); - } else if (desc.get || desc.set) { + continue; + } + if (desc.get || desc.set) { // 替换 getter/setter 的 this 为实际的 global window。 overriddenDescs[key] = materializeDescriptor(desc, realmGlobal); descsCache.add(key); // 必须:子类属性覆盖父类属性 @@ -259,7 +261,9 @@ const createGlobalSnapshot = ({ realmGlobal, hostWindow }: RealmRoots): GlobalSn } else if (!(key in initOwnDescs) && !Object.hasOwn(realmGlobal, key) && !protoBaseDescs[key]) { protoBaseDescs[key] = materializeDescriptor(desc, hostWindow); } - } else if (desc.get || desc.set) { + return; + } + if (desc.get || desc.set) { overriddenDescs[key] = materializeDescriptor(desc, hostWindow); descsCache.add(key); }