|
|
b4610f1539
|
fix(安全)★: index.html 加 CSP —— 渲染层的最后一道防线(实测过不误伤)
**为什么现在没有 XSS**:全库 `dangerouslySetInnerHTML` / `innerHTML` /
`insertAdjacentHTML` 命中 **0**,`react-markdown` 无 `rehype-raw`,
`test/markdown-xss.test.mjs` 钉着。⇒ 这条 CSP **不是**为了堵今天的洞,
而是防「有人加一行 `dangerouslySetInnerHTML` 之后那道防线消失」。
★ 而 `sandbox: true` + `contextIsolation: true` **替不了这一层**:
那两条只挡 node 与跨上下文,**不挡渲染层注入**。
## ★★ 不是照抄模板 —— 每一档都按仓库真实用法定的
· `script-src` **必须**含 `'unsafe-inline'`:上方那段首帧防闪屏脚本要求
**同步**执行(早于任何外链,见 index.html 头部注释),无法外链化。
⇒ 因此 CSP **挡不住注入型 XSS**,只挡 `javascript:` URL / 外部脚本 / `eval`
(`script-src` 不含 `'unsafe-eval'`)。★ 这一点已写进 meta 上方的注释 ——
否则下一个人会以为这是严格 CSP。要去掉就得外链化那段脚本,而那会**引入闪屏**。
· `style-src` 同理 + `img-src`/`font-src` 含 `data:`:
`backgroundStore.ts:252-262` 用 `root.style.setProperty('--bg-image', url("data:image/…"))`
⇒ 背景图就是 data URL。
· `connect-src` **只能是 `'self'` + 宽松一档**:网关地址**用户可填**
(`AccountList` 的 placeholder 就是「Gateway 地址,如 http://192.168.2.60:8180」),
可以是**任意 host:port** ⇒ 这里没法写成 allow-list。
真正的防线在渲染层与主进程(导航拦截),不在这一行。
· `object-src 'none'` / `base-uri 'self'` / `frame-ancestors 'none'` / `form-action 'self'`
是纯收紧,**零兼容代价**。
## ★ 真的用 chromium 实测过它不误伤(不是推理)
无头加载一份带同款 CSP 的页面,结果写进 DOM:
```
id="R"> pending | inline-ran | cssom-ran | eval-BLOCKED
```
⇒ 内联脚本跑(防闪屏有效)、CSSOM 设 data: 背景有效、`eval` 被拦。
且构建产物 `dist/index.html` 里 CSP 确实在位(`npm run build` 后 grep 得到)。
## 判据(markdown-xss 9 → 19,已接线)
新增 10 格。变异测试 5 个全部抓住:删整条 CSP / 去掉 `object-src 'none'` /
去掉 `frame-ancestors` / 去掉 `img-src` 的 `data:` / 加 `'unsafe-eval'`。
★ 顺带把「防闪屏脚本必须是单引号 `classList.add('dark')`」也钉在这里 ——
`index.html` 的注释声称 `test/theme.test.mjs` 在逐字符断言它,
**实测 theme.test.mjs 并不断言引号**(它只切 `.dark {}` 色块)⇒ 那条注释是**过期的**,
历史上被 prettier 改写过一次的事件其实**没有判据在挡**。现已由本条补上。
★ 取 CSP 值时踩了两个坑(都记在判据注释里):`[^>]*` 会在策略里的
`'self'>` 处提前结束;按行匹配会因 `<meta>` 跨行而只取到第一行。
⇒ 判据不匹配标签,只确认 meta 存在再单独取 `content="…"`。
## 边界 / 未做
· CSP 只覆盖**浏览器/Web 侧**;Electron 的 `file://` 加载下 meta CSP 仍生效,
但主进程那道防线是 `will-navigate` / `setWindowOpenHandler`
(`test/main-process-security.test.mjs` 钉着)。
· **没在真机上验过 CSP** —— Electron `loadFile()` 那条路未实测
(只验了 chromium 下的 `file://`)。下次装机时顺手看一眼首帧有没有闪。
|
2026-10-03 21:21:55 +08:00 |
|
|
|
ec90cba129
|
test(criteria): 抽出共享 check/finish(marker 不再靠记性)+ 失败信息自带修法
pi 的三条增量,前两条落地:
1. **错误信息自带修法**:受众不只是读过规范的人 —— 并发写 WebUI 的 agent 新加判据时不会打开
CRITERIA.md,看到红的第一反应可能是"套件坏了"。所以把可照抄的修法写进那条错误本身
(共享 helper 的用法 + 样板文件路径),并说明 node:test 的判据不用管。
**red 是 ta 一定会看到的,文档不一定被打开。**
2. **marker 由共享 helper 打印**:新增 test/lib/checks.mjs(导出 check/finish),
计数只可能在该模块内发生 → "漏打 marker"与"计数写错位置"这两类在新文件上不可能发生。
为避免"写了没人用"(本仓踩过的坑),同时把两个手工计数的判据改用它:
narrow-layout(原来只有 failed 计数)与 markdown-xss(原来根本没有计数器)——
条数不变(52 / 9),套件仍全绿。
未回改其余 10 个文件:run-all 的 marker 检查已经覆盖它们。
|
2026-09-14 15:11:47 +08:00 |
|
|
|
a8ac2fc28b
|
test(criteria): 闭环——判据自报条数 + 每文件期望条数(只增不减的棘轮)
pi 指出的残余缺口:我上一轮加的自检 4 是**文本证据**(文件里有 `test(` / `check(` /
`process.exit(1)`),只能证明"**有能红的路径**",不能证明"**它跑过**"。反例很短:
```js
const check = () => {}; // 实现被换空(现实形态:合并冲突改坏实现)
check('a', false); // 存在、也执行了,但什么都不会红
console.log('主题:通过'); // 有输出
```
## 落地(pi 给的闭环形状)
1. 自定义 `check()` 的判据结尾打一行机器可读汇总 `RESULT pass=<条数> fail=<失败数>`
(`node:test` 的判据不用改,已有 `# pass N`);
2. `run-all.mjs` **只解析这个固定 marker**(不猜口语汇总——「窄屏布局:全部通过」里没有数字,
按数字猜会误报,这一点我上轮已经实测过);
3. 与清单里登记的**期望条数**比对,**低于 → 红**。
关键细节:**计数写在 `check()` 内部**(theme/background 原本就在内部 ++;
narrow-layout 只有 failed 计数,补了 passed;markdown-xss 按 payload 条数算)。
写在调用点或靠扫源码的话,"实现被换空"就看不见了。
棘轮"只增不减":加判据**不用**改那个数,只有"条数掉了"才红。期望值按**实测**回填
(9/52/8/30/42/15/28/5/23/5/3/2)。
附带的可见性收益:这几轮我一直用"13→14""19→28""34→42"当信号,现在它成了判据 ——
某次改动顺手删掉两条判据、或某条被跳过,会立刻红。
## 变异
- pi 那个反例(`check` 换成空函数)→ 红(`自报 0 条 < 登记的 30 条`);
- 删掉 5 条 `check(` 调用 → 红(`自报 47 条 < 登记的 52 条`)。
## 规范
§6.5 新增"涉及运行时行为的结论必须实测过才能写进规范/判据"——同一个错这轮犯了两次
(我从"报告 0 个测试、退出 0"推断"退出码被吞",实测是照传;pi 拿我这个结论又建了一个洞)。
规则:**一次观察只支撑你看到的那一层**。
§6.6 记闭环形状与代价(故意删判据要同步改数字,属于一次可复核的显式编辑)。
⚠️ 并且如实记下一次**我自己违反规范**的事:写 §3 那条"变异后别用 `git checkout` 还原"的人
(就是我)在这次变异验证里又用了 `git checkout -- <文件>`,把刚加、尚未提交的 marker 抹掉了。
规矩写下来不等于会遵守 —— 已把这条实例写进规范,让人知道它是活人踩的坑。
## 验证
`npm test` 退出码 0(12 个判据文件全绿 + vitest 258/258);`run-all` 单独跑也 exit 0。
|
2026-09-14 15:06:14 +08:00 |
|
|
|
f9d757b5e5
|
chore: directory migration - gateway→server, web→client/electron
|
2026-09-08 19:16:35 +08:00 |
|