🐛 修复 Firefox 下 @inject-into content 脚本沙盒缺失 EventTarget 方法 - #1692
Open
CodFrm wants to merge 2 commits into
Open
Conversation
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,第二趟 全部跳过。
cyfung1031
self-requested a review
August 27, 2026 09:39
cyfung1031
reviewed
Aug 27, 2026
cyfung1031
left a comment
Collaborator
There was a problem hiding this comment.
应该可以。你测试过没问题就merge吧
有问题再修
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Checklist / 检查清单
N/A — 未找到对应的 open issue,问题来自用户直接反馈(Firefox 上运行沉浸式翻译脚本报
TypeError: globalThis.addEventListener is not a function,Chrome 正常)。背景
在 Firefox 上运行带
@inject-into content的用户脚本(如沉浸式翻译)时,脚本抛出:同一脚本在 Chrome 正常。因果链:
@inject-into content命中isInjectIntoContent()(src/app/service/content/utils.ts:284),getUserScriptRegister()把该脚本注册到world: "USER_SCRIPT"(
src/app/service/service_worker/utils.ts:233-236),而非默认的MAIN。create_context.ts以global为基准收集 PropertyDescriptor 构造,global经 rspack 编译为__webpack_require__.g,即globalThis。USER_SCRIPTworld 里globalThis === window(一个真Window);Firefox 用的是
Cu.Sandbox,globalThis是伪装成Window的 Sandbox 对象,其原型链在 Xray window 处截断,够不到
EventTarget.prototype。getAllPropertyDescriptors永远收集不到addEventListener/removeEventListener/dispatchEvent,沙盒缺失这三个方法。在真 Firefox 154 的 content world 实测(同一段流水线,两个 world 对照):
globalThis === windowtruefalseWindow → Window → WindowProperties → EventTarget → ObjectWindow → Window(2 层到头)addEventListener归属EventTarget.prototypeNOT FOUND IN CHAINaddEventListenerfunctionundefined只有
EventTarget.prototype上那三个方法丢失;setTimeout/fetch/document/location/alert/localStorage/XMLHttpRequest均正常(Xray 已把它们平铺为Sandbox 的自有属性)。
本次改动
create_context.ts的描述符收集由一趟改为两趟,收集回调参数化,bind目标跟随当轮根物件:descsCache原本就是「先到先得(子类覆盖父类)」,天然给出正确优先级:第一趟收下的键不会被第二趟覆盖。Chrome 下
window === globalThis,第二趟全部命中descsCache而跳过,行为完全不变。
diff 中 56/45 行大半是回调缩进位移,实质改动为参数化 + 增加一次调用。
实现考虑
为什么不是把基准直接换成
window。 这是首个尝试的方案,实测证明会引入回归:Firefox 的Sandbox 是独立 realm,其 78 个自有属性几乎全是 JS 内置物件(
Array/Number/Object/JSON/Math/Promise/Symbol/Temporal/WebAssembly…);而经 Xray 看页面 window 的同名内置物件会被剥到只剩壳:
Number自有属性length,name,prototype(3 个)isNaN/isInteger/EPSILON)Math自有属性String自有属性一刀切换成
window后,真扩展验证中沉浸式翻译改抛TypeError: Number.isNaN is not a function(修复前该错误 0 次,一刀切版本 2 次)。两趟收集才能两头都对:JS 内置取自本 realm,DOM 原型链取自真实 window。
为什么
bind目标必须跟随 root。 两个根分属不同 realm,把EventTarget.prototype的方法绑到 Sandbox 会触发 brand check 失败;绑到
window后实测可正常挂监听并被dispatchEvent触发。已知限制
以下两项均已实测修复前后逐字节一致,非本 PR 引入,按 scope discipline 未在此处理:
@inject-into content的脚本里globalThis指向 ScriptCat 沙盒(
typeof globalThis.GM_getValue === "function"),但window/self逃逸到页面真实window(
typeof window.GM_getValue === "undefined")。同一根因的延伸 ——create_context.ts中自引用改写的判定desc?.value === global在 Firefox 下不成立(Sandbox 的
window属性值是 Xray window,不等于globalThis),自引用未被改写为返回
mySandbox的 getter。这属于沙盒隔离缺陷,建议单独开 issue 处理。TypeError: can't access property "call", getter is null,修复前后均存在;修复后出现次数增加,仅因脚本不再于首个爆点中断、执行到了更多代码。
本 PR 仅覆盖 Firefox 的 content / USER_SCRIPT world;
MAINworld 与 Chrome 全部路径行为不变。
建议审查重点
descsCache保证(第一趟已收键在第二趟被descsCache.has(key)提前 return)。
else if (!(key in initOwnDescs) && !Object.hasOwn(root, key))中由global改为root是否符合预期:initOwnDescs仍取自global,第二趟中EventTarget.prototype的方法两个条件皆成立。
window === globalThis⇒ 第二趟全跳过)。关联
以下 issue 均标记
FirefoxMV2(本问题在 MV3 路径,strict_min_version 154.0),未验证是否同源,不作 close 关联,仅供审查时参考:
[BUG] ScriptCat + Firefox: TypeError: can't access property "then", GM.getValue(...) is undefined[BUG] inject-into无法正确绕过csp限制的bug验证
按
docs/references/develop-testing.md的第二条TDD 例外(「a bug reproducible only in a specific browser version … platform behavior that can't be
automated reliably. Verify it manually and record the evidence」)走手工验证:happy-dom 测试环境下
globalThis === window,该缺陷在单测中无区分力,无法写出会失败的常规单测。提交的检查
全量中 1 项 UI flaky(
src/pages/下的useInstallData/McpFormDialog/Agent/Tasks/SettingsPane,失败项在多次运行间变化)。已将本改动git stash后在HEAD 上跑同一条全量命令,失败模式完全一致(1 failed / 4277 passed),为仓库既有 flaky;
单独运行这 4 个文件 87 项全过。
Firefox 真扩展验证
pnpm run build产物 +createFirefoxManifest()生成解包目录,经 geckodriver(
--allow-system-access)安装为临时扩展。userScripts与<all_urls>在 Firefox MV3下是可选权限、需用户手势,headless 无法点击授权弹窗,故改由 chrome context 调用
ExtensionPermissions.add()预置(等价于用户在权限弹窗点「允许」)。脚本经serviceWorker/script/installByCode种入。构建产物已核对确实包含两趟调用(
tr(h.g),tr(window),e9.clear())。探针脚本(与沉浸式翻译同为
@inject-into content):typeof=undefined | THREW=TypeError: globalThis.addEventListener is not a functionael=function/fired=1 | builtins_ok=7/7 | Math.max(1,2)=2 | Number.isNaN(NaN)=true沉浸式翻译真脚本(v1.32.7,
installByCode返回name: "Immersive Translate: AI Web, PDF & Video Translator"),开
devtools.console.stdout.content统计页面 console:addEventListener is not a functionNumber.isNaN is not a functionScreenshots / 截图
N/A — 非视觉改动,证据为上述终端输出与浏览器 console 统计。