Skip to content

🐛 修复 @include 脚本代码被注入到所有页面 (#1803) - #1814

Open
CodFrm wants to merge 2 commits into
mainfrom
fix/include-injection-scope
Open

CodFrm wants to merge 2 commits into
mainfrom
fix/include-injection-scope

Conversation

@CodFrm

@CodFrm CodFrm commented Nov 2, 2026 •

Copy link
Copy Markdown
Member

Checklist / 检查清单

  • Fixes mentioned issues / 修复已提及的问题
  • Code reviewed by human / 代码通过人工检查
  • Changes tested / 已完成测试

背景

#1803 中,在不匹配的页面(如起点)的 DevTools 里能看到只该在抖音/B站运行的脚本源码,window['#-<uuid>'] 上挂着完整代码。脚本没有执行(执行由 service worker 的匹配把关),但任何网站都能枚举出用户装了哪些脚本并读取源码,每个页面也要多解析这些代码。

根因:chrome.userScripts(Firefox 134+ 同样)对 matches 与 includeGlobs 取并集,网址命中任一即注入。getApiMatchesAndGlobs 原先按「先 matches 再用 includeGlobs 过滤」设计,遇到 @include glob 或正则就把 matches 放宽成 *://*/*,再指望 includeGlobs 收回范围,结果所有带 @include 的脚本都被注入到了每个 http(s) 页面。

在 Chrome 148 上实测:matches: ["https://nomatch.invalid/*"] 加 includeGlobs: ["*example*"] 会在 example.com 注入;matches: ["*://*/*"] 加 includeGlobs: ["*douyin*"] 同样会在 example.com 注入;excludeMatches/excludeGlobs 正常生效;matches 不能为空。

本次改动

1. 按并集语义注册(01d121d7)

  • matches 只放脚本真正的 match pattern。只有 glob 时用 *://scriptcat.invalid/* 占位(.invalid 是保留域名,API 要求 matches 非空),注入范围完全由 includeGlobs 决定。
  • 能抽出 scheme://网域 的正则 @include 转成小写的网域级 glob。正则默认忽略大小写,glob 区分大小写。
  • 结尾不是 * 的 glob 补一条 <glob>#*:实测 Chrome 拿带 #hash 的完整网址匹配 glob,而 SW 匹配前会先去掉 hash。
  • 修改 CompiledResourceNamespace,旧的注册信息在升级后失效并重建。
  • 删除改动后已无调用方的 addMatchesToGlobs、extractMatchPatternsFromGlobs。

2. 正则收不窄的脚本改由 pageLoad 下发代码(c2ef01a9)

  • 对抽不出网域的正则(例如没有 scheme 的 /foo\.com/),getUserScriptRegister 标记 deliverOnPageLoad,这类脚本不预注册。
  • SW 在 pageLoad 命中后,把挂载 window[flag] 的注入代码放进 TScriptInfo.code。scripting 把它转交给 content,content 沿用旧版 createElementNS 插入 <script> 的写法注入页面,inject 照常通过 flag 接手。
  • 由 content 注入而不是 scripting:content 运行在 USER_SCRIPT world,其 configureWorld CSP 放行内联脚本,插入页面的 <script> 不受页面 CSP 和 Trusted Types 限制(在 github.com、youtube.com 实测)。scripting 所在的 ISOLATED world 插入的脚本会被拦。
  • 以下情况仍全站预注册:@early-start(需在页面脚本之前同步挂载)、@unwrap(注入即执行),以及 Firefox(其 USER_SCRIPT world 插入的 <script> 受页面 CSP 限制)。

实现考虑

  • 执行时机:普通脚本原本就要等 pageLoad 往返才执行(代码虽在 document_start 挂上,但要等 SW 下发脚本信息)。按需下发只多一次 content 内的同步插入,执行时机基本相同。
  • 没有选 chrome.userScripts.execute:它需要 Chrome 135+,而 content 注入在 Chrome 120+ 都能用,也不需要剥页面 CSP。
  • 精确注册后,Chrome 自己的 glob 匹配开始真正生效,必须保证注册范围不比 SW 的匹配窄,否则原本能运行的脚本会被漏掉。#hash 和正则大小写两处已处理(见上)。

已知限制

  • 用了收不窄正则的 @early-start / @unwrap 脚本,以及 Firefox 上的这类脚本,代码仍会挂到所有页面(维持原状)。
  • Firefox 例外依据的是此前 Firefox E2E 中 GM_addElement 在 CSP 页面失败的记录,本次没有在 Firefox 上重新验证。
  • glob 精确注册后,以单独一个 ? 结尾的网址(如 /page?)不会注入:SW 视其为匹配,Chrome 不视为匹配,属罕见情况。
  • 顺带发现但不在本 PR 范围内的两处:src/app/service/agent/service_worker/dom.test.ts 改写 chrome.permissions 后未还原,在 isolate: false 下导致后续文件的 setup 失败;e2e/drive.mjs 的 sw 命令在页面注册了自己的 Service Worker 时会拿错 worker。

建议审查重点

  • src/pkg/utils/url_matcher.ts 的 getApiMatchesAndGlobs:确认注册范围始终不比 SW 匹配窄,尤其是网域 glob 转换和 #* 变体。
  • src/app/service/service_worker/runtime.ts:deliverOnPageLoad 的脚本在批量注册、单脚本变更(需撤销旧注册)和 pageLoad 三条路径上的处理是否一致。
  • url_matcher.test.ts 大部分期望值按新契约重写(旧期望写死了 AND 语义下的 *://*/*),可对照新旧 diff 确认没有遗漏原有分支。

关联

Close #1803

验证

以下结果绑定 base e88d0eb6(含 #1815 的 Playwright 1.64.1)、head 91f93aa9。本 PR 先基于 eecd34a2 开出,#1815 合并后 rebase,无冲突。

  • CI(run 41385339524,head 91f93aa9):Lint、单元测试两个分片、Run tests、Runtime smoke、E2E 四个分片全部通过。rebase 前 Runtime smoke 的失败,是因为 ubuntu 26.04 runner 不支持 Playwright 1.60,由 ⬆️ 升级 @playwright/test 到 1.64.1,修复 ubuntu 26.04 runner 上的 CI 失败 #1815 修复。
  • 修复前复现:在 Chrome 148 上,带正则 @include 的脚本在 example.com 上挂着 window['#-<uuid>'];只有纯 @match 的对照脚本没有。
  • 新增 E2E e2e/include-injection-scope.spec.ts:在 origin/main(eecd34a2)的构建上 1 failed / 1 passed,无关页面泄漏了 2 个 #-uuid;在本分支上 2 passed。
  • rebase 后本地复跑(Playwright 1.64.1 / Chrome Headless Shell 156):
    • pnpm exec playwright test e2e/runtime-bootstrap.spec.ts e2e/popup-matching-regressions.spec.ts e2e/gm-api.spec.ts e2e/include-injection-scope.spec.ts:18 passed,其中 gm-api 页内自测 138 项通过、0 失败(含严格 CSP 页面)。
    • pnpm exec vitest run --no-coverage src/pkg/utils src/app/service/service_worker src/app/repo src/app/service/content:79 个文件、1805 条全部通过。
    • pnpm run typecheck 通过。
  • TDD:新增和改写的单元用例都在改实现之前先确认为失败。
  • 本地全量单元测试:本地全量 pnpm test 不可靠,origin/main 原样运行也有 79–140 个文件失败,且每次数量不同。原因是高负载下超时,以及 dom.test.ts 的 mock 泄漏。全量结果以上面 CI 的两个分片为准。
  • 静态检查:eslint、prettier 通过;文档中提到的标识符已在已提交代码中用 git grep 核对。
  • 范围:git diff e88d0eb6...91f93aa9 共 12 个文件,均属于两项改动、对应测试和执行路径文档。

chrome.userScripts 与 Firefox(134+)对 matches 和 includeGlobs 取并集:网址命中任一即注入。
getApiMatchesAndGlobs 原先按「matches 之后再用 includeGlobs 过滤」设计,遇到 @include glob 或
正则就把 matches 放宽成 *://*/* 再指望 includeGlobs 收回,结果所有带 @include 的脚本代码都挂到了
每个 http(s) 页面(执行仍由 service worker 的匹配把关,但源码对页面可见)。

- matches 只放脚本真正的 match pattern;只有 glob 时用 *://scriptcat.invalid/* 占位(API 要求 matches 非空)
- glob 不以 * 结尾时补一条 #* 变体:浏览器拿带 hash 的完整网址匹配 glob,脚本猫匹配前会去掉 hash
- 能抽出 scheme://网域 的正则转成小写的网域级 glob;抽不出的仍全站注册,并以 regexUnbounded 标记
- 修改 CompiledResourceNamespace,让已保存的旧注册信息失效重建
能抽出 scheme://网域 的正则已转成网域 glob 精确注册;抽不出的(如没有 scheme 的局部正则)只能全站注册,
脚本代码仍会挂到所有页面。这类脚本改为不预注册:service worker 在 pageLoad 匹配命中后,把挂载
window[flag] 的注入代码放进 TScriptInfo.code 下发,scripting 转交给 content 环境,content 沿用旧版
createElementNS 插入 <script> 的写法注入页面,执行环境照常通过 flag 接手。

content 运行在 USER_SCRIPT world,其 CSP 放行内联脚本,插入页面的 <script> 不受页面 CSP 与 Trusted Types
限制(github.com、youtube.com 实测);scripting 所在的 ISOLATED world 做不到,所以不能由它注入。

仍全站预注册的例外:early-start 需在页面脚本前同步挂载、unwrap 注入即执行,都等不了 pageLoad;
Firefox 的 USER_SCRIPT world 插入的 <script> 受页面 CSP 限制。

新增 e2e/include-injection-scope.spec.ts:glob(含带 #hash 的网址)与收不窄的正则脚本在各自目标页执行
(正则目标页下发 script-src 'none'),且不在无关页面留下 window[flag]。该用例在 origin/main 上失败。
@CodFrm
CodFrm force-pushed the fix/include-injection-scope branch from c2ef01a to 91f93aa Compare November 2, 2026 03:51
@cyfung1031

cyfung1031 commented Nov 2, 2026 •

Copy link
Copy Markdown
Collaborator

SC机制不一样

SC用 API 先在浏览器层面注册
SC的管理器(scripting/content/inject) 只是决定运不运行读不读取删不删除

还是会在 DevTools 看到的
当然 window['XXXXX'] 那个设计可以改进
还是那一句:不要搞一个 auto remove flag. 默认 auto remove 就好。没有保留必要。取了function就可以吞掉

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

在DevTools看到疑似 加载了多余的js,应像Tampermonkey一样不显示

2 participants