CC Switch 把「信任渲染进程」写成可证伪的决定
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 命中 |
| 2 | CSP 把脚本执行限制在打包资源内——原文写的是 script-src 'self' | 读 src-tauri/tauri.conf.json 的 csp 字段 | 不再逐字成立,见第四节 |
| 3 | 无动态代码求值,src/ 下不存在 eval() 或 new Function() | grep -rnE "new Function\(|[^a-zA-Z_.]eval\(" src/ | 0 命中 |
| 4 | 全库唯一的 dangerouslySetInnerHTML 在 src/components/ProviderIcon.tsx,用户提供的 icon 字段仅作为查表的键 | grep -rn "dangerouslySetInnerHTML" src/ | 1 处,位于 :79 |
第 4 条是四条里最值得多看两眼的一条,因为它的写法与前三条不同。前三条说的是「某个东西不存在」,第 4 条承认了 sink 是存在的,然后把安全性押在参数的来源上:ProviderIcon.tsx:29-33 里,iconSvg 来自 getIcon(icon),取自 @/icons/extracted 这张构建期的图标表;用户提供的 icon 只在经过 isUrlIcon 与 hasIcon 判断后被当作键使用,真正渲染出去的是表里查到的内容,发生在 :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:29 的 csp 字段实际是这样的(只截取 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。能做到这一点,是因为这份文档的结构本身提供了三样东西:
- 决定挂着事实,事实挂着位置。 四条支撑事实全部落在
src/、src-tauri/tauri.conf.json这些确切的文件上,第 4 条甚至精确到组件名。位置一给,核对就从「讨论」降级成了「搜索」。 - 失效条件是预先写好的判据。
:55那一条让「CSP 加了个 sha256 白名单算不算事」有了现成答案,不需要临时争论一个标准出来。 - 核实声明带版本号。
:26那句「已针对 v3.18.0 核实」把这一节的时效边界写在明面上,读者读到的第一秒就知道它是对哪个版本负责的。
同一份文档里还有另一处用了同源的手法::86-93 的两条豁免都是先给出豁免,再用「以下情形不属豁免」的反例把边界重新圈回范围内——同样是把一句容易被各自理解的话,改造成可以逐条比对的形态。
六、你自己复核时的几个实际问题
想重跑一遍第二节那张表,需要注意两件事。
一是命令环境。上面那几条 grep -rn 是在提供 GNU grep 的环境里跑的(macOS 与 Linux 自带,Windows 侧需要在 Git Bash、WSL 或同类环境里执行)。如果你的 Windows 机器上没有这些环境,就别照抄命令了——用你手上任意一个支持正则的全仓库搜索工具,按同样的关键词(<iframe、<webview、new Function(、eval(、dangerouslySetInnerHTML)搜一遍即可,重点是关键词和搜索范围(限定在 src/ 下),不是命令本身的写法。
二是读 CSP 那一条不要用搜索代替。csp 在 src-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)。
本文内容为仓库源码与文档口径,我们没有安装或运行过这个桌面应用,
因此不涉及界面外观、操作手感与切换速度的任何描述。
文中出现的阈值与默认值均为源码中的默认配置,不构成对实际运行结果的保证。
该项目仍在快速迭代,版本与默认值随时可能变动,请以仓库最新内容为准。
安全相关做法请结合自身环境评估,本文不构成安全方案建议。
留言讨论
评论发布后会被人工复核,违规内容将被删除。
如果发表没有反应,可以前往联系我们告诉我们。