Skip to content

✨ 增加 CSP 规则管理#1598

Draft
cyfung1031 wants to merge 8 commits into
scriptscat:mainfrom
cyfung1031:agent/csp-rules-management
Draft

✨ 增加 CSP 规则管理#1598
cyfung1031 wants to merge 8 commits into
scriptscat:mainfrom
cyfung1031:agent/csp-rules-management

Conversation

@cyfung1031

@cyfung1031 cyfung1031 commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator

Checklist / 检查清单

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

背景

部分用户脚本在特定网站上会被 CSP response headers 阻止执行。ScriptCat 需要提供按网站管理的 CSP 规则,同时保留浏览器现有动态规则,并让高风险的全网站范围操作具备明确确认和可恢复的错误状态。

由于扩展使用 split incognito 模式,普通窗口和隐私窗口可能运行不同的 service worker。CSP 规则状态必须明确归属,不能依赖两个后台上下文之间的内存状态同步。

本次改动

  • 在 Tools 页面新增 CSP Rules 卡片:
    • 规则总开关;
    • 新增、编辑、删除规则;
    • 单条规则启用/停用;
    • 空状态、分页展示、加载错误和应用错误状态;
    • 100 条规则时禁用新增入口。
  • 支持按规范化域名匹配网站,并支持 All websites 高风险范围。
  • All websites 在创建、编辑切换和启用前都要求确认。
  • 新增版本化本地 state DAO(csp_rule:state):
    • 校验 schema、revision、规则数量和域名数量;
    • 读写失败与缺失 key 分离;
    • 保留持久化数据,避免异常读取时误认为空状态。
  • 新增纯 DNR compiler 与固定 ID 2001 applier,只管理 CSP response header removal 规则,并保留其他 dynamic rules。
  • 保留 incognito: "split",只在普通 service worker 注册 CSP rule handlers;隐私窗口显示普通窗口管理提示,不影响既有隐私窗口扩展页面流程。
  • 表单打开时固定 baseRevision;父级收到新快照后,旧表单仍使用打开时的 revision,冲突时刷新快照并关闭表单。
  • 编辑已有规则时不提供独立的 Enabled 开关,避免规则状态与编辑 patch 不一致。
  • 保存进行中禁用 Save、Cancel、范围控件和确认操作,避免重复提交。
  • 新增 8 个 locale 的完整 tools 文案,用户可见文本全部通过 i18n。
  • 新增 service、DAO、compiler、manifest 和 UI 回归测试。

实现考虑

1. CSP 状态只由普通 service worker 持有

Manifest 继续使用 incognito: "split",以兼容现有从隐私窗口打开确认页、安装页和其他扩展页面的流程。

CSP service 通过 isCspRuleOwner(extensionEnv.inIncognitoContext) 明确限制为普通上下文:

  • 普通 service worker 读取、保存并应用 CSP 状态;
  • 隐私 service worker 不注册 CSP rule handlers;
  • Tools 页面在隐私窗口不读取 CSP state,并展示可操作的普通窗口提示。

这样不会通过全局切换到 spanning 模式改变既有隐私窗口行为,也不会让两个后台进程各自持有可互相覆盖的 CSP 内存状态。

2. revision 校验覆盖多个 service instance

每次 mutation 都会:

  1. 在 DAO 的独占队列中执行;
  2. 重新读取并校验持久化 state;
  3. 对比调用方提供的 baseRevision
  4. 只有 revision 一致时才保存并应用下一版本;
  5. 写后重新读取,确认持久化结果与预期一致。

两个 service instance 共享 DAO 并竞争同一 revision 时,一个请求成功,另一个返回 revision_conflict,不会静默覆盖对方修改。

3. 启动失败保持可恢复

无效 schema 或不支持的数据会先尝试移除旧的 CSP rule,但清理失败不会让 service 进入永久拒绝状态。

初始化错误会保留为可恢复状态,retryApply 可以重新初始化、重新清理并应用默认状态。storage read failure 也会明确返回错误,不会被当作缺少 state key。

4. 表单与高风险操作保持一致

编辑表单使用打开时捕获的 revision,而不是父级后来收到的最新 revision。

编辑已有规则时 Enabled 状态只能通过列表中的单独操作改变;因此切换 All websites 时,确认条件始终基于真实规则的 enabled 状态。保存请求进行中锁定表单操作,避免双击产生第二个 mutation。

5. 保留可操作校验反馈

service 返回的 messageKey 会透传到 UI,域名、规则数量、唯一域名数量和 patch 校验错误显示对应的本地化提示,而不是统一显示 storage error。

已知限制

1. CSP 规则管理仅在普通窗口提供

隐私窗口不显示 CSP state 和编辑入口,用户需要从普通窗口管理规则。这样保留 split incognito 下现有扩展页面行为,并避免隐私与普通 service worker 之间出现无法原子协调的状态竞争。

2. 规则只移除 CSP response headers

当前规则类型固定为 removeCspHeaders,仅移除匹配响应中的 CSP headers,不会添加或修改用户自定义 CSP policy,也不会绕过其他浏览器安全机制。

3. DNR 应用失败需要重试

state 会先持久化,再应用到 DNR。若浏览器拒绝或暂时无法更新动态规则,UI 会保留已保存 state 和失败 revision,并提供 Retry;受影响页面在应用成功前可能仍使用上一版本规则。

4. 域名范围和规则数量有限制

每条规则最多包含 100 个域名,全局最多包含 1,000 个唯一域名和 100 条规则。域名按 hostname 规范化,路径、凭据和通配符不会作为有效域名保存。

建议审查重点

  • split incognito 下是否始终只有普通 service worker 注册 CSP handlers;
  • 两个 service instance 竞争同一 revision 时是否始终返回一个成功和一个 revision_conflict
  • storage read failure、无效 schema 和旧 CSP rule 清理失败是否都保留可恢复路径;
  • 编辑表单收到新的 stateChanged 后是否仍使用打开时的 baseRevision
  • 编辑 All websites、启用 All websites 和总开关恢复时是否都不会绕过确认;
  • 保存进行中是否阻止重复提交、关闭表单和确认操作;
  • DNR compiler 是否只管理 CSP rule ID 2001 并保留其他 dynamic rules;
  • 所有新增 UI 文案、校验错误和隐私窗口提示是否覆盖 locale 与 i18n 使用检查。

参考

docs/specs/csp-rule-management.md

docs/develop.md

docs/design.md

验证

  • pnpm test:ci — 310 test files, 3297 tests passed
  • pnpm lint:ci — Prettier, TypeScript, and ESLint passed
  • pnpm build — passed (repository bundle-size/Monaco warnings only)

cyfung1031 and others added 8 commits July 17, 2026 14:58
保存/启用/禁用/删除 CSP 规则时,若 chrome.runtime.sendMessage 的响应在
service worker 挂起后丢失(例如用户填写表单期间 SW 进入空闲),本地会把
这类"响应丢失"和服务端明确的失败混为一谈,直接提示保存失败——而实际上
突变已经在后台成功执行,用户重试时才会看到 revision 冲突,此时才发现
其实已经保存成功。现在 CspRuleClient 用 CspRuleAmbiguousResponseError
单独标记这种不确定情形,UI 侧据此重新拉取权威 state 校验 revision 是否
已推进,从而正确识别为成功或失败。

删除操作的确认气泡(Popconfirm)此前被嵌套在 DropdownMenuItem 内,与
Radix 的 DropdownMenu 共用可关闭层(dismissable layer)冲突,气泡在用
户尚未点到确认按钮前就随下拉菜单一起被关闭。现在删除改为复用本文件已有
的顶层 AlertDialog 确认模式(与"启用所有网站"确认一致),不再嵌套弹层。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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.

1 participant