# 安全审计修复说明与待决事项 > 生成时间:2026-09-19 | 审计对象:本仓库 + `192.168.5.7` 上运行的 `kole-ui-showcase` 容器 > 本文档分两部分:**已修复**(我直接做了,含证据)与 **待你拍板**(我做不了主的,逐项给选项与建议)。 --- ## 一页速览 | 状态 | 数量 | 说明 | |---|---|---| | 已在仓库修复并验证 | 6 项 | 见第二节,全部有 exit 0 证据 | | **已部署并在线上验收** | 全部 | 2026-09-19 重建容器:6/6 断言 + 396/396 URL + 响应头全项通过,见第三节 | | 仍待你拍板 | 4 项 | D3 / D4 / D6 / D8(D1、D2、D5、D7、D9、D10 已决,见第四节开头) | | 过程异常 | 4 项 | 见第五节 E1–E4(你已确认无其它会话,属当时并发写入) | **一句话**:洞已堵上且已上线——线上访问 `specs/组件1.txt` 现在返回 404,站点 396 条 URL 全部 200。 --- ## 二、已修复(仓库层,全部已实测验证) ### 1. 规范原文泄漏的根因(`.dockerignore`) **问题**:`.dockerignore` 里 `*.md` / `*.ps1` / `*.yml` / `*.png` 这类写法,在 Docker 下**只匹配构建上下文的根目录**。实测(真实 docker build): ``` .dockerignore: *.md COPY . /out/ -> /out/sub/nested.md ← 嵌套文件照样进镜像 ``` 所以 `.design_library/kole-ui/SKILL.md`、嵌套 `README.md` 等一直会被打进镜像;而部署目录 `/opt/kole-ui` 干脆**没有** `.dockerignore`,于是 `specs/`、`agent-reports/`、`preview/`、`ui_kits/` 全部随 `COPY .design_library/` 进了镜像并对外可下载。 **修复**:glob 全部加 `**/` 前缀,保留显式目录排除与 `!README.md`(只放行根级 README)。 **验证**(我在服务器上用当前文件做真实构建): ``` 应排除 -> specs/ 0 | agent-reports/ 0 | preview/ 0 | ui_kits/ 0 | nested.md 0 | AGENTS.md 0 | docker-compose.yml 0 应保留 -> site/index.html 1 | site/data.json 1 | site/tokens/tokens.css 1 | site/sources/x.txt 1 frameworks/*.css/.vue3 1 | tests/index.html 1 | tests/report.json 1 .design_library/kole-ui/components/button.json 1 | README.md 1 BUILD_EXIT=0 ``` ### 2. `nginx.conf` 加固 - 补 `absolute_redirect off;` —— 修复根路径 302 丢失宿主端口(`Location: http://192.168.5.7/site/`,而该机 :80 无服务 → 根地址直接打不开)。 - 补 `server_tokens off;` —— 不再暴露 `nginx/1.31.3`。 - 补 CSP + `X-Content-Type-Options` + `Referrer-Policy` + `Permissions-Policy` + `X-Frame-Options`,CSP 与 `site/dev-server.js` 里的 `SECURITY_HEADERS` **逐字一致**(本地长期验证可用的组合,避免过严导致演示页白屏)。 - `/healthz` 改用 `default_type`,消除重复的 `Content-Type`(原先是 `return 200` 的默认类型与 `add_header` 叠加)。 **验证**:子 agent 在真容器里跑 `nginx -t` → `syntax is ok` / `test is successful` / `exit 0`;并做了修复前/修复后运行时对照(`Location` 由绝对变相对、`Server` 由 `nginx/1.31.3` 变 `nginx`、`/healthz` 由两条 Content-Type 变一条且不丢安全头、gzip 行为等价)。 ### 3. `Dockerfile` 补 `COPY sitemap.xml /usr/share/nginx/html/sitemap.xml`(此前生产 `/sitemap.xml` → 404)。 未动基础镜像 tag、未加 `USER`(nginx 官方镜像 master 需 root 绑 80,擅自改会让容器起不来)——运行期加固见 D6。 ### 4. `site/dev-server.js`:单请求崩溃 **问题**:`GET /site/%00` → `decodeURIComponent` 解出 NUL → `fs.readFile` 同步抛 `ERR_INVALID_ARG_VALUE` → 异常逃逸出请求回调 → **整个进程退出**(畸形百分号编码反而处理正确)。CI 与本地回归都跑这个服务。 **修复**:编码形态拦截补 `00`/`2500`、解码后**再做一次** NUL 校验(双保险)、`readFile` try/catch。未使用 `uncaughtException` 兜底(那是掩盖)。行为不变:92 条合法路径 status / content-type / 字节级一致。 **验证**:`node tools/verify-dev-server.mjs` → **20 checks / EXIT=0**,含 3 条「修复前必失败」的反例断言(进程存活、突发后仍 200、stderr 无崩溃栈)。 ### 5. 消除动态求值 sink(运行期 + 构建期) **问题**:`site/app.js` 与 `tools/precompute.mjs` 都把 `defineEmits([...])` 的数组字面量交给函数构造器求值。实测: ``` 输入 defineEmits([1, globalThis.__pwned = "executed-arbitrary-js"]) 旧实现 -> 真的执行了 ``` 即:任何能改 `frameworks/*.vue*` 的人(投毒 PR / 内部威胁)在组件详情页被打开时即可执行同源 JS;构建期同源版本**在构建机上执行**,而 CI 往往带 secrets。 **修复**:改成只解析字符串字面量的 `parseStringLiteralArray`——非字符串元素 / 未识别转义 / 裸换行 / 结构不符一律整体拒绝返回 `null`(跳过抽取、不报错、绝不执行)。构建期实现独立成 `tools/lib/parse-string-literal-array.mjs`。 **等价性验证**:`node tools/verify-emits-parse.mjs` → **17 checks / EXIT=0**,158 个 `frameworks/*.vue` 双实现抽取结果**不一致 0**(54 处字面量 / 96 条事件名),且新实现对 6 类对抗输入全部拒绝、不执行。 ### 6. 新增验证脚本与变更记录 - `tools/verify-dev-server.mjs`(20 checks)、`tools/verify-emits-parse.mjs`(17 checks)——均为零依赖、`exit 1` 表示失败、含「修复前会失败」的反例断言。 - `CHANGELOG.md` 追加 `[Unreleased] → Security · 生产部署链审计修复(2026-09-19)`。 - ⚠️ **规划修正**已在 CHANGELOG 标注:`PLAN.md` §8「不改 `site/dev-server.js`(已工作良好)」的立论被实测推翻,按 AGENTS.md 第五节「以实测为准」处理。 ### 回归证据 ``` 连跑 9 次(子 agent):每次 passRate 100% | pages 79 (all-pass 79) | 1017/1017 | N/A 34 | rc=0 主 agent 独立复跑 1 次(独立端口 13411):passRate 100% | 1017/1017 | N/A 34 | EXIT=0 零依赖铁律:zero-dep OK(EXIT=0) ``` --- ## 三、已部署(2026-09-19) 按 AGENTS §九 的标准流程部署(备份 → 暂存目录核对 → 替换 → `docker compose build && up -d`)。共执行两次: 1. **修复落地**:把审计修复 + 工作树当前状态推上线(消除 288 文件漂移)。 2. **重新部署**:`npm run build:site` 两步构建重生成 `data.js` / `data.json` / `site/sources` / `site/details` 后,再跑一次打包流程保持线上与仓库一致(实测线上 `/site/data.js` 102,555B、`/site/data.json` 1,251,099B,与仓库一致;`data.json` 仍为完整版,符合铁律 5)。 回滚点: - `/opt/kole-ui.pre-audit-20260919-0609` —— 审计前的原始状态(含被泄漏的 `specs/`、旧 `nginx.conf.bak`) - `/opt/kole-ui.prev-20260919-0614` —— 第一次修复部署后的状态 - `/root/kole-ui-backup-20260919-0609.tgz`(877 KB)、`/root/kole-ui-backup-20260919-0614.tgz`(882 KB) | 项目 | 修复前(实测) | 修复后(实测) | |---|---|---| | `/.design_library/kole-ui/specs/组件1.txt` | **200 / 8623B**(规范原文可下载) | **404** | | `/.design_library/kole-ui/agent-reports/*.json` | 200 | **404** | | 嵌套 `SKILL.md` / `README.md` | 200 | **404** | | `/AGENTS.md`、`/package.json`、`/Dockerfile`、`/nginx.conf` | 404 | 404(一直未泄漏) | | `/sitemap.xml` | 404 | **200** | | `tests/report.json`(首页通过率角标数据源) | 404 | **200** | | 根路径 302 | `Location: http://192.168.5.7/site/`(丢端口,落点无服务) | **`Location: /site/`**(相对,保留端口) | | 响应头 | 无安全头,`Server: nginx/1.31.3` | CSP + `nosniff` + `Referrer-Policy` + `Permissions-Policy` + `X-Frame-Options`,`Server: nginx` | | `/healthz` | 两条 `Content-Type` | **1 条**,且安全头未丢 | | `/site/dev-server.js` | 旧版(无白名单/无敏感路径过滤) | 加固版(`isPublicPath` + `KOLE_PORT`) | | 组件文件 | 09-06 快照,与仓库漂移 288 个文件 | 与工作树一致 | | sitemap 的 396 条 URL | 396/396 → 200 | **396/396 → 200**(无回归) | | 容器 | healthy | healthy | 对 Agent 承诺的资源(`.design_library/kole-ui/{metadata.json,components/index.json,css.json,colors_and_type.css}`)仍正常 200 —— 排除的只是规范原文、agent-reports、preview、ui_kits 这些不该外发的部分。 **访问方式**:按你的决定保持 `127.0.0.1:3311` 回环。经实测确认,服务器上**没有任何 frp 隧道 / 反代 / 域名指向该服务**,所以从任何其它机器都打不开是必然的——唯一的访问路径是 SSH 隧道: ```bash ssh -o ExitOnForwardFailure=yes -N -L 13311:127.0.0.1:3311 root@192.168.5.7 # 浏览器打开 http://127.0.0.1:13311/site/ (本机 13311 是为了不占本地 dev-server 的 3311) ``` 已实测:经隧道对线上部署跑完整文档站冒烟(真实浏览器)全部通过——路由、中英切换、主题面板、详情页渲染均正常。 若想让**局域网内其它设备**也能直接打开 `http://192.168.5.7:3311/`,需把 compose 绑定改为 `0.0.0.0:3311` 并注意 Docker 会绕过 ufw —— 这属于显式暴露变更,等你说一句我就改。 --- ## 四、待你拍板(D1–D10) > **2026-09-19 决定记录**(依据你的指示「暂时不考虑线上公用,但是还是得部署,修改原本的设计和计划」): > > | 项 | 结论 | 落地位置 | > |---|---|---| > | D1 是否部署 | ✅ **已部署**(按流程:备份 → 暂存核对 → 替换 → 重建;6/6 断言 + 396/396 URL 通过) | 本文第三节、CHANGELOG `Deploy` 段 | > | D2 部署方式 | ✅ **已改为可复现**:新增 `tools/pack-deploy.mjs`(`.dockerignore` 为唯一真源 + 硬断言),流程写进 AGENTS §九 | `tools/pack-deploy.mjs`、`AGENTS.md §九`、`ROADMAP S5-P13` | > | D5 端口暴露 | ✅ **保持回环**、不对外公用;AGENTS §九 明确「打不开是正常的,用 SSH 隧道」 | `AGENTS.md §九` | > | D7 PLAN §8 冻结条款 | ✅ **已按实测修正**(§7 与 §8 两处都标注 ⚠️ 规划修正) | `PLAN.md`、`CHANGELOG.md` | > | D9 测试基线口径 | ✅ **已同步**为 1017 通过 / 0 失败 / 34 N/A / 共 1051(标注口径来源) | `TESTING.md`、`AGENTS.md §八`、`ROADMAP 规划修正记录` | > | D10 是否还有其它会话 | ✅ **你已确认无**;本次审计中观察到的是当时并行写入,后续已复核无冲突 | 本文第五节 E1–E4 | > > **仍未决**:D3(`tests/report.json` 是否随镜像发布)、D4(正式域名)、D6(容器运行期加固)、D8(契约对外承诺口径)。这四项已登记为 ROADMAP 任务包 S5-P14/P15/P16,等你有空再定。 ### D1|是否现在重建并重新部署线上容器?【最高优先】 - **选项 A(建议)**:只同步「本次修复的文件集」重建,不动组件文件。范围小、可回滚、立即堵住规范原文泄漏与掉端口问题。 - **选项 B**:把当前工作树整体同步(顺带消除 288 文件的漂移)。代价:会把仓库里 286 个**未提交**的组件改动一起上线(这些改动本地回归 100%,但等于一次完整发布)。 - **选项 C**:暂不部署,只保留仓库修复。 **我的建议**:先做 A(堵漏),B 作为一次独立「发布」动作、等工作树改动提交审阅后再做。 选 A 时的执行序列(等你一句话我就执行): ```bash # 1) 备份现状(在 192.168.5.7 上) cd /opt/kole-ui && tar -czf /root/kole-ui-$(date +%Y%m%d-%H%M).tgz . # 2) 只更新 5 个文件:nginx.conf / Dockerfile / .dockerignore / site/dev-server.js / site/app.js # (加上新增的 tools/lib、tools/verify-*.mjs 可选) # 3) 重建并重启 docker compose build && docker compose up -d # 4) 验收断言(四条必须全中) curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:3311/.design_library/kole-ui/specs/组件1.txt # 期望 404 curl -sI http://127.0.0.1:3311/ | grep -i '^Location' # 期望 /site/ curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:3311/sitemap.xml # 期望 200 curl -sI http://127.0.0.1:3311/healthz | grep -ci '^content-type' # 期望 1 ``` ### D2|部署方式:怎么根治「手工拷贝导致漂移」 **这就是这次事故的根因**:`/opt/kole-ui` 是手工拷贝的目录,无 git、无 `.dockerignore`,所以 ① 排除规则失效导致规范泄漏;② 曾经修好的 `absolute_redirect off;` 被下一次拷贝覆盖(它只存在于 `nginx.conf.bak-20260916`,从未进过仓库 git 历史)。 - **选项 A(建议)**:改用 rsync/脚本按固定排除清单同步 + 重建,排除清单与 `.dockerignore` 保持同一份真源。 - **选项 B**:服务器上 `git clone` 仓库 + `docker compose build`,用 git 版本作为唯一真源。 - **选项 C**:保持现状手工拷贝(不推荐,同样的回退会再来一次)。 ### D3|`tests/report.json` 要不要随镜像发布 - **现状**:当前 `.dockerignore` **保留**它 → 重建后首页「通过率」角标会在线上显示(子 agent 一度排除,被我方流程外的改动改回保留,并在文件里写了理由)。 - **选项 A(建议)**:保留 —— 首页那个「79 测试页 + 通过率」卡片本来就在页面上,显示数字才算兑现。 - **选项 B**:排除 —— 镜像更干净,但卡片静默不显示(当前线上就是 404 → 静默隐藏)。 - 若选 B,删掉 `.dockerignore` 中相关保留注释对应的行即可回滚。 ### D4|sitemap 与包的正式域名(`SITE_URL_BASE`) `sitemap.xml` 内容目前是 `https://YOUR-ACCOUNT.github.io/kole-ui/…`(占位域名),`package.json` 的 `repository` / `homepage` 同样是占位。 - **需要你给**:正式域名(或明确「不做 SEO,接受占位」)。 - `build-site.ps1` 已支持通过环境变量 `SITE_URL_BASE` 覆盖;`tools/release.mjs` 已有占位符闸门会拦住正式发布。 - 顺带一提:`tools/verify-site-routing.mjs` 目前**断言 sitemap 里必须含占位 URL**——如果换了真域名,这个断言要同步改(否则校验会失败)。 ### D5|端口暴露策略 - **现状**:绑定 `127.0.0.1:3311`,只有本机/SSH 隧道能访问(我从局域网直连 `192.168.5.7:3311` 超时,符合预期)。 - **选项 A(建议)**:保持回环,并在 README/交接文档写明「必须隧道访问」,避免有人以为服务挂了。 - **选项 B**:挂 1Panel 反代 + 域名(要不要加 Basic Auth / IP 白名单?)。 - 影响面:选 B 会让规范原文泄漏面从「本机」变成「公网」,必须先把 D1 做完。 ### D6|容器运行期加固(需要一次重启窗口) `read_only: true` + `tmpfs`、`cap_drop: [ALL]`、`security_opt: no-new-privileges`、基础镜像 digest 固定。 好处明确,但改 compose 后必须重建容器,且 `read_only` 配错会让 nginx 起不来(需要临时可写 `/var/cache/nginx`、`/var/run`)。要不要做、什么时候做,你定。 ### D7|`PLAN.md` §8 冻结条款的修正 `PLAN.md` §8 写着 `❌ 不改 site/dev-server.js(已工作良好)`,§7 有对应验收条目。实测推翻了「已工作良好」,我已按 AGENTS.md 路径修了代码并在 CHANGELOG 标注 ⚠️ 规划修正,但**没有改 `PLAN.md` 本身**(它在冻结清单里)。 需要你确认:是否把该行改成「dev-server 的畸形路径处理属必修项」。 ### D8|契约 JSON 的对外承诺怎么统一 - **本地**:79 份契约 + `index.json`。 - **线上**:只有 `button/card/input/modal/select/table.json` + `index.json`(7 个文件),`tag.json`、`calendar.json` 等 → 404。 - **文档三处互相矛盾**:`site/llms.txt` 写「契约覆盖全部 79 个组件」;`AGENTS.md` 写「契约 JSON 79」;而 `index.json` 自己的 description 写「其中 **6 个核心组件**另含契约 JSON」。 **选项**:A 全量发布 79 份(镜像略大,但对 Agent 消费的承诺为真);B 只发布核心 6 份,并把 `llms.txt`/`AGENTS.md` 的表述统一改为「6 份核心契约 + 79 组件索引」;C 保持现状(不推荐,等于承诺失实)。 ### D9|测试基线数字的口径同步 `TESTING.md` 记的是 `1009 / 35`,HEAD 提交信息也写「回归1009-1009」,而当前工作树实测是 **1017 通过 / 34 N/A**。差异来自工作树里已有的未提交改动,不是本次修复造成。建议在合并这批改动后统一同步(我没改 `TESTING.md`,它不在允许清单内)。 ### D10|是否还有别的会话/自动化在动这个仓库 见第五节 E1–E3。**如果有,建议先停掉再合并**,否则两边互相覆盖。这是本次最需要你确认的运维问题。 --- ## 五、必须让你知情的过程异常 **E1|`.dockerignore` 在我方子 agent 交付后被第三方进程改写。** 子 agent A 交付的哈希是 `3b34ffa6`(其中排除了 `tests/report.json`),磁盘现状是 `843cb650`(保留 `report.json` 并写了理由注释)。功能上现状更优(首页通过率卡片能显示),但 A 报告的 47 条断言**不覆盖**现状版本——所以我用当前文件独立重做了真实构建复验(第二节 1,全过)。这也意味着我已无法声称「这个文件的每处改动都来自本次派活」。 **E2|`tools/precompute.mjs`(05:56:41)与 `tools/lib/parse-string-literal-array.mjs`(05:56:45)不在任何子 agent 的允许清单内,却被写入了;且 05:56:29 有一次我没派出的构建运行。** 那次运行重写了 **494 个文件**(`site/sources/**` 395、`site/details/*` 79、`site/data.js`、`site/data.json`、`site/changelog.json`),但 `sitemap.xml` 与 `site/tokens/*` 未变——即只跑了第二步 `node tools/precompute.mjs`,没跑 `build-site.ps1`。 我复核了内容与后果: - `tools/precompute.mjs` 的 `node --check` 通过、`tools/lib/...mjs` 可加载、与 `site/app.js` 在 158 个 `.vue` 上抽取结果一致、对恶意输入不执行; - **重建后的 `site/data.js` 哈希 `5fdffe24…` 与构建前、与线上部署三方完全一致** → 这次构建是语义无损的,同时**独立证明了新的安全解析器不改变构建输出**(不依赖我方验证脚本的口径); - 漂移比对从我审计时的「288 个文件不同」变为「289」,差额恰好是本次修复的 `site/app.js`,其余无新增漂移。 **结论**:改动立意是对的(构建期确实有同一个求值 sink,打穿的是构建机而非浏览器),但来源不明、未走派活流程。若这是你的另一个会话在做同一件事,请注意两边会互相覆盖(见 D10)。 **E3|共享模块头部引用的 `tools/verify-precompute-emits.mjs` 并不存在。** 且注释里描述的「源码逐行等价 + 5000 例种子随机等价」比实际存在的检查更强。我已把注释改为描述**真实存在**的门(`tools/verify-emits-parse.mjs`:158 文件结果等价 + 对抗语料),改动后复跑该脚本仍 `17 checks / EXIT=0`。 若你确实想要那道更强的门(源码等价 + 随机 fuzz),需要另派一个任务实现。 **E4|服务器 `/tmp` 残留 `kole-di-final*`(非本次产物)。** 我清理了自己创建的全部临时目录与 scratch 镜像;这三项来源不明,未删以免干扰可能仍在运行的会话。生产容器 `kole-ui-showcase` 全程未动(`Up 2 days (healthy)`)。 --- ## 六、审计中被验证为「无问题」的部分(免得重复怀疑) | 检查项 | 结果 | |---|---| | 路径穿越(`../`、`..%2f`、`%2e%2e`、`....//`、反斜杠、`.git/config`) | 全部 403 | | 搜索框 XSS(``) | 0 个注入节点、0 次执行 | | 路由滥用 | 无效 slug / 越界路径全部回落首页,无异常 | | 代码高亮渲染 | 先全量转义再还原 token,无注入 | | 硬编码凭据 | 未发现(命中的都是 design-token 变量名) | | 零运行时依赖铁律 | 成立(`dependencies` 为空) | | sitemap 里的 396 条 URL | 线上 396/396 → 200(静态页无死链;sitemap 文件本身 404 另见第二节 3) | | 容器健康 | `healthy`,`/healthz` 200 | --- ## 附录:本次审计与修复用到的复验命令 ```bash # 全量回归(需先起服务;服务端口可用 KOLE_PORT 覆盖) KOLE_PORT=13411 node site/dev-server.js & REG_BASE=http://127.0.0.1:13411 node tools/run-regression.mjs # 两处缺陷的定点验证 node tools/verify-dev-server.mjs # 20 checks,含 3 条反例断言 node tools/verify-emits-parse.mjs # 17 checks,158 文件双实现等价 # 铁律 node -e "const p=require('./package.json');const d=Object.keys(p.dependencies||{});if(d.length){console.error('FAIL');process.exit(1)}console.log('zero-dep OK')" # 部署配置复验(在 192.168.5.7 上,用一次性容器,不碰生产) docker run --rm -v /tmp/<你的>/default.conf:/etc/nginx/conf.d/default.conf:ro nginx:alpine nginx -t ``` 证据留档:`C:\Users\12914\Desktop\组件规范第一套\.zcode\deployed-manifest.txt`(线上 1394 文件哈希清单)、`.zcode\drift-check.mjs`(漂移比对脚本)、`.zcode\sitemap-check.txt`(396 条 URL 清单)。