CC Switch 把「信任渲染进程」写成可证伪的决定

2026-08-31

cc-switch 仓库根目录的 SECURITY.md 里有一节叫「打包的渲染进程属于信任边界之内」(:26-37)。「把打包的渲染进程当成可信的」这句话本身只是一个表态,读者除了接受或拒绝没有别的动作可做;而这一节把它拆成了三段可以逐条比对的东西:一句自称决定的元声明、四条能用搜索命令复核的支撑事实、五条提前写死的失效条件。

更有意思的是,这套写法在仓库里已经自证过一次:四条支撑事实中的一条,在文档最后一次改动之后不到三周,被另一个方向的前端修复推翻了——第四节会把这处差异落到确切的行号与提交上。

本文依据的是 CC Switch v3.20.1 的仓库快照(commit 3217f725),核对日 2026-08-31。全程只是静态读源码与文档、跑几条 grep、看几条 git log没有编译、没有运行、也从来没有安装过这个桌面应用,所以下面不会出现任何关于界面、操作或速度的描述。

一、元声明:这是决定,不是推论

SECURITY.md:26(英文)与 :28(中文)给出的元声明是这样的:打包的 WebView 渲染进程被视为可信组件,「这是一项范围划定决策,由下列事实支撑,而非从中必然推出——这些事实的作用是让该决策可被核验;一旦任一条不再成立,该决策必须重新评估。已针对 v3.18.0 核实」。

这一句里塞了三层信息,每一层都在给后面的内容定性质:

  • 它自称 decision(决策),不是 conclusion(结论)。决策可以被质疑取舍,结论只能被判对错,这两者面对的反驳方式完全不同。
  • 事实的角色被限定为「让决策可核验」,而不是「决策由此推出」。这条限定避免了一种常见的循环:把四条查得到的现象堆在一起,装作它们已经证明了某个更大的主张。
  • 核实声明带版本号 v3.18.0。也就是说,这一节从写下的那一刻起就是有时效的,时效边界写在纸面上,读者不用猜。

二、四条支撑事实,每一条都对应一个可执行的复核动作

SECURITY.md:30-37 列了四条。它们的共同特点是:不描述意图,只描述能被搜索到的状态。

#文档措辞(:30-37)可执行的复核动作v3.20.1 快照下的结果
1不载入远程可执行内容;代码库中不存在 <iframe><webview> 或远程脚本/样式地址grep -rniE "<iframe|<webview" src/0 命中
2CSP 把脚本执行限制在打包资源内——原文写的是 script-src 'self'src-tauri/tauri.conf.jsoncsp 字段不再逐字成立,见第四节
3无动态代码求值,src/ 下不存在 eval()new Function()grep -rnE "new Function\(|[^a-zA-Z_.]eval\(" src/0 命中
4全库唯一的 dangerouslySetInnerHTMLsrc/components/ProviderIcon.tsx,用户提供的 icon 字段仅作为查表的键grep -rn "dangerouslySetInnerHTML" src/1 处,位于 :79

第 4 条是四条里最值得多看两眼的一条,因为它的写法与前三条不同。前三条说的是「某个东西不存在」,第 4 条承认了 sink 是存在的,然后把安全性押在参数的来源上:ProviderIcon.tsx:29-33 里,iconSvg 来自 getIcon(icon),取自 @/icons/extracted 这张构建期的图标表;用户提供的 icon 只在经过 isUrlIconhasIcon 判断后被当作键使用,真正渲染出去的是表里查到的内容,发生在 :79

这条差别值得留意:一个 HTML sink 存在与否,本身说明不了什么;能说明问题的是「谁能控制流进它的那个值」。文档没有把这个 sink 藏起来,而是把它单列一条、写清参数来源——于是这条主张变成了任何人都能顺着 :29-33:79 两处位置自己核对的东西。

顺带一句分寸:上面表里的「0 命中」「1 处」都是 v3.20.1 快照当天的搜索结果,下一个版本随时可能变。这恰恰是它们应当被写成「复核动作」而不是「结论」的理由——命令固定,结果每次重跑。

三、五条失效条件:先把作废的判据写死

紧接着,SECURITY.md:53-62 列出五条失效条件,前置措辞是「一旦下列任一条件成立,上述范围划定立即作废」:

  • 远程可执行或可导航内容被载入 WebView——iframe、webview、远程脚本、远程样式表,或导航至远程源;
  • script-src 被放宽到 'self' 之外;
  • 引入 eval()new Function()
  • 任何用户可控字符串作为内容进入 HTML sink;
  • IPC 接口暴露给非打包来源。

这五条与第二节的四条几乎是一一对照的镜像:支撑事实说「现在是什么状态」,失效条件说「变成什么状态就不算数」。把作废判据提前写死的直接后果是,后来的人不需要重新论证「这算不算破坏了信任边界」,只要比对判据就行。

四、第二条已经不逐字成立了

现在看第 2 条。

文档 SECURITY.md:32 写的是「CSP restricts script execution to bundled assets — script-src 'self'src-tauri/tauri.conf.json)」。而在 v3.20.1 快照里,src-tauri/tauri.conf.json:29csp 字段实际是这样的(只截取 script-src 一段):

script-src 'self' 'sha256-ls27Lc1OytcVawXPVMplz6LyKz2i9KYAVBw3u79qyB4='

'self' 之外,多了一条针对某段内联脚本的 sha256 精确白名单。这段哈希是跟着那段内联脚本走的,脚本一改,哈希就得跟着改,所以它同样不是一个可以长期抄用的常量。

变更来源可以直接查出来。git log -S "sha256-ls27Lc1" -- src-tauri/tauri.conf.json 指向提交 d4fefefc,日期 2026-08-16,提交信息是 fix(windows): eliminate startup white-black flash (FOUC) (#6252)。而 SECURITY.md 本身的最后一次改动是 2026-07-29(同样可以用 git log -1 --format='%ad' --date=short -- SECURITY.md 自己重数一遍)。两个日期摆在一起,时间线是清楚的:这份安全文档比它所描述的那行配置早了 18 天,此后没有再更新过。

按照写差异的规矩,这里必须把分寸拿准,说完就停:

  • 一方面,sha256- 白名单与 'unsafe-inline' 那种普遍放宽不是一回事——它精确锁定某一段特定的内联脚本内容,脚本内容一变哈希就对不上。
  • 另一方面,文档的字面表述(script-src 'self')与它自己设下的失效判据(「script-src 被放宽到 'self' 之外」)都已不再逐字成立。这不是我们外部加的标准,是文档 :55 自己写的那一条。
  • 四条支撑事实里,另外三条在 v3.20.1 仍然对得上(无 iframe/webview、无动态求值、唯一的 HTML sink 只把用户字段当查表键)。所以这是四分之一失效,不是整节失效。

本文只陈述这两处位置的差异,不推断该由哪一侧去调整,也不据此评价这个项目或它的维护方式。

五、这套写法在这个仓库里换来了什么

值得注意的是,上面这一整段核对过程,全部是隔着屏幕做的:我们没有联系任何人,没有跑这个应用,只用了四条搜索命令和两条 git log。能做到这一点,是因为这份文档的结构本身提供了三样东西:

  1. 决定挂着事实,事实挂着位置。 四条支撑事实全部落在 src/src-tauri/tauri.conf.json 这些确切的文件上,第 4 条甚至精确到组件名。位置一给,核对就从「讨论」降级成了「搜索」。
  2. 失效条件是预先写好的判据。 :55 那一条让「CSP 加了个 sha256 白名单算不算事」有了现成答案,不需要临时争论一个标准出来。
  3. 核实声明带版本号。 :26 那句「已针对 v3.18.0 核实」把这一节的时效边界写在明面上,读者读到的第一秒就知道它是对哪个版本负责的。

同一份文档里还有另一处用了同源的手法::86-93 的两条豁免都是先给出豁免,再用「以下情形不属豁免」的反例把边界重新圈回范围内——同样是把一句容易被各自理解的话,改造成可以逐条比对的形态。

六、你自己复核时的几个实际问题

想重跑一遍第二节那张表,需要注意两件事。

一是命令环境。上面那几条 grep -rn 是在提供 GNU grep 的环境里跑的(macOS 与 Linux 自带,Windows 侧需要在 Git Bash、WSL 或同类环境里执行)。如果你的 Windows 机器上没有这些环境,就别照抄命令了——用你手上任意一个支持正则的全仓库搜索工具,按同样的关键词(<iframe<webviewnew Function(eval(dangerouslySetInnerHTML)搜一遍即可,重点是关键词和搜索范围(限定在 src/ 下),不是命令本身的写法。

二是读 CSP 那一条不要用搜索代替。cspsrc-tauri/tauri.conf.json:29 是一个很长的字符串,script-src 只是其中一段,直接打开文件读整行比 grep 一个片段更不容易看漏。

最后一点:以上所有行号、默认值与搜索结果都属于 v3.20.1 这一个快照,这个项目迭代很快,下次你去看的时候文件行号很可能已经挪位。真正能沿用的,是这份文档给出的那个结构——一个决定、一组带位置的支撑事实、一组预先写死的失效条件。它不保证结论永远正确,它保证的是结论错了的时候有人能指出来。


本文依据 CC Switch 官方仓库(github.com/farion1231/cc-switch)的 README、docs/ 下的用户手册、 路由指南与发布说明,以及 src/src-tauri/tests/ 的源码整理, 核对日 2026-08-31,对应仓库快照 3217f725(仓库内版本号 3.20.1)。 本文内容为仓库源码与文档口径,我们没有安装或运行过这个桌面应用, 因此不涉及界面外观、操作手感与切换速度的任何描述。 文中出现的阈值与默认值均为源码中的默认配置,不构成对实际运行结果的保证。 该项目仍在快速迭代,版本与默认值随时可能变动,请以仓库最新内容为准。 安全相关做法请结合自身环境评估,本文不构成安全方案建议。

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。

留言讨论

评论发布后会被人工复核,违规内容将被删除。

    还没有人评论,来说说你的看法

    如果发表没有反应,可以前往联系我们告诉我们。

    在 CC Switch 里加一个国内直连的供应商

    力达云网关,注册送 ¥5 额度,一期提供 DeepSeek。

    去添加

    这个页面有问题?

    提交时会附带当前页面地址和浏览器信息,帮助我们定位问题。不填联系方式即为匿名。