Ctrl K
ComunidadGuías…

docs/security-audit.md

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# 阶段 7 安全与轻量资源审计

本文保留各阶段的历史审计与浏览器测试证据。Playwright 已移除;当前浏览器验证方式见[浏览器验收清单](./browser-acceptance.md)。

初始审计日期:2026-07-30。全仓复审与修复日期:2026-07-31。

## 2026-07-31 全仓复审修复

- 首次 Web 初始化必须使用进程启动时生成的 `isb_...` bootstrap token。日志只在未初始化时输出原文,服务内仅保存 SHA-256,成功后立即清除。
- Argon2 维持约 19 MiB 参数并全局串行执行;setup 在 KDF 前检查初始化状态和 token,登录与改密按凭据串行,避免内存放大和旧密码会话竞态。
- 上传与历史路径导入在读取正文前抢占处理槽,使用单遍 multipart 流并只落一次 `/data/tmp`;GIF 在完整解码前完成帧预算检查。
- 删除、可见性修改和 WebP 转换按 image ID 串行;转换无法生成新缩略图时会移除旧缩略图。
- Nginx Proxy Manager 部署默认信任代理地址头,按 `X-Real-IP`、最右有效 `X-Forwarded-For`、TCP 对端顺序取客户端 IP;原生监听默认仅为 `127.0.0.1:8080`,容器镜像明确监听 `:8080`。
- 标准 URL、别名、迁移目录和缩略图共享可配置的读取并发门;默认 `0` 表示不限制,正整数上限满载时返回 `503`。HTTP 服务同时设置请求头、正文、响应和空闲期限。
- 托管文件通过 root-scoped 打开;启动和重建会将文件大小不符的记录排除交付,巡检会把它们报告为不可用;迁移目录还会校验扩展名对应的真实图片 MIME。
- SQLite 数据目录收紧为 `0700`,主库及现存 WAL/SHM 收紧为 `0600`。
- 前端统一处理 401、清理用户查询缓存;Modal 具备焦点陷阱和背景 inert;筛选 URL、日期时区、批量快照、分页、设置草稿、失败原因、键盘操作和受限 localStorage 均有回归测试。
- 发布工作流验证 tag 提交属于 `main`,使用 fail-closed GHCR 查询、提交绑定 content tag、平台 digest 和幂等 manifest;benchmark 仍只跑并发 1,同时加入 CPU、内存、PID、成功率、p95 和峰值门禁。
- Docker build context 改为白名单;远程脚本仅接受规范化的 `/var/tmp/imagesilo-*`,名称参数受限且不再递归改权。TailAdmin 与 Outfit 的许可证随源码及镜像分发。

为保持轻量,图片正文不会复制进 Go 全图缓存,也不会在每次启动对全部图库重新计算 SHA-256;交付使用内存元数据索引、ETag、文件流和操作系统页缓存。启动与巡检执行廉价的存在性、普通文件和大小校验,卷本身仍属于可信本地存储边界。React Router 当前唯一生产依赖 High 公告仅影响未使用的 RSC API,继续作为明确例外记录;新增 npm 生产依赖 Critical 会阻断 release。项目自身采用何种许可证仍需仓库所有者选择,本次没有擅自代选。

本轮本机原生 arm64 生产镜像大小为 `118,770,769` 字节。受限 benchmark 使用 1 CPU、768 MiB、256 PID、处理并发 1 和 16 个连续 16 MP 转 WebP 请求:16/16 成功、busy 为 0、p95 为 `1,725.309 ms`,cgroup 当前 `139,796,480` 字节、峰值 `345,550,848` 字节,其中 anon `84,873,216` 字节、file `52,105,216` 字节。10 万图片加 10 万别名的交付索引为 `148.70` bytes/path,100 万次标准/别名命中及 miss 的 `lookupFailures` 均为 0。

## 已自动化的边界

| 风险 | 自动化证据 |
|---|---|
| 路径穿越、双重编码和保留路由 | `internal/delivery/path_test.go`、`internal/platform/storage/filesystem_test.go` |
| Session 轮换、CSRF、登录限速和安全响应头 | `internal/httpapi/phase_two_test.go` |
| 私密图片、Token Scope、吊销、过期和越权删除 | `internal/httpapi/phase_two_test.go`、`internal/httpapi/phase_five_test.go` |
| 损坏、伪装、截断和超像素图片 | `internal/platform/processor/processor_test.go` |
| 标准 URL/别名、Range、416、ETag、条件请求和 HEAD | `internal/httpapi/delivery_handler_test.go`、`scripts/container-smoke.sh` |
| 上传/删除/巡检/重建后的临时文件 | `internal/httpapi/phase_seven_test.go`、`scripts/container-smoke.sh` |
| goroutine、文件描述符和优雅停止 | `scripts/container-smoke.sh`、`scripts/remote-release-acceptance.sh` |
| 非 root、固定 UID/GID、只读数据目录错误和精简镜像 | `deploy/docker/Dockerfile`、`scripts/container-smoke.sh` |

容器协议检查只追加 4 次串行公开读取;公开交付的两个重叠读使用微型内存夹具,仅证明并发安全,不是压力测试。持续图片处理 benchmark 仍是每架构并发 `1`、总计 `16` 个请求。

## 依赖扫描

`govulncheck` 曾在实际调用链发现 `golang.org/x/image v0.38.0` 的两个 WebP 解码漏洞:`GO-2026-5061` 和 `GO-2026-4961`。依赖已原位升级到修复两项的 `v0.43.0`,没有增加新模块或服务。

`npm audit` 报告 React Router 的 `GHSA-qwww-vcr4-c8h2`。官方公告说明它只影响 unstable RSC APIs,ImageSilo 的 Vite SPA 没有 RSC、SSR、Server Action 或框架模式;截至审计时 npm 尚未发布公告标注的 8.3.0 修复版。该项作为不可达代码路径的临时例外记录,并在每次发布前复查。

## 镜像审计

本机原生 arm64 构建的 Docker 未压缩大小为 `117,585,383` 字节。最终镜像中的 ImageSilo 二进制为 `10,754,496` 字节,精简后的 `/opt/vips/lib` 为 `2,562,441` 字节;不包含 Node、Go、编译器、源码、测试素材或 libvips 的头文件、pkg-config、静态库和命令行工具。

受限验收脚本先在本机以 `0.5` CPU、`256m` 内存、处理并发 `1` 自测 10 秒,共完成 5 轮串行 health/readiness/标准 URL/别名读取。cgroup 当前内存 `30,154,752` 字节、峰值 `30,932,992` 字节,goroutine `10 → 10`,文件描述符 `10 → 10`,临时文件为零。

阶段 7 对 16 MP 固定最坏样本追加了 effort 1/4 A/B。effort 1 的单请求峰值和耗时一度分别下降约 13.9% 和 31%,但双架构连续 16 请求后匿名内存升到 arm64 `249,716,736` 字节、amd64 `258,211,840` 字节,容器峰值也反而超过 `497 MB`。因此该候选被撤回,最终继续使用 effort 4;轻量决策以持续工作集为准,不以单请求速度为准。

最终 [Verify run 30479364047](https://github.com/Willxup/imagesilo/actions/runs/30479364047) 在原生 amd64/arm64 上全部通过。16 个 WebP 输出合计 `89,923,072` 字节;两个架构结束文件页缓存均约 `91.4 MB`,与输出总量吻合。结束匿名内存分别为 amd64 `86,945,792` 字节、arm64 `101,023,744` 字节,证明约 397 MB 的 cgroup 峰值不能全部解释为进程泄漏。

外部原生 amd64 共享主机从提交 `b866eeb` 构建候选:专用 BuildKit 固定到 CPU 0、1 CPU quota、1 GiB 内存和 1 GiB memory+swap 上限,完成后删除。随后容器以 0.5 CPU、256 MiB、128 PID、处理并发 1 连续运行 600 秒,完成 118 轮串行字节校验;cgroup 当前 `14,135,296` 字节、峰值 `35,938,304` 字节、结束匿名内存 `8,814,592` 字节,goroutine `10 → 8`、FD `10 → 10`,最终 SIGTERM 退出码为 0。证据归拢在专用 `/var/tmp/imagesilo-*` 目录;builder、容器和临时候选镜像均已移除。

空库运行基线继续记录在 `docs/performance-baseline.md`。镜像磁盘大小不等于运行内存,阶段 7 仍以实际 RSS、Go heap、cgroup peak、goroutine 和文件描述符为资源事实来源。

## 发布证据

`v0.1.0-rc.1` 的 tag push 自动触发 [Release image run 30505989284](https://github.com/Willxup/imagesilo/actions/runs/30505989284),质量门、原生 amd64/arm64 构建、完整容器 smoke、平台镜像推送和 manifest 校验全部成功。工作流已移除手动触发入口,只接受 `v*` tag push。

公开 OCI manifest 为 `sha256:573233d00455ab6e8dad9d875ad0ede7f770436ce24bb05f6d21bc02fac053cc`;amd64 子 manifest 为 `sha256:ee410a57912c125234e5547520a1f5b38a253649cfeec88aec34d53903838ad8`,arm64 子 manifest 为 `sha256:a5437e2348e74a9aaa53e0ac7f685c379153d9183362651e1a88a077f9f9452d`。独立匿名 GHCR token 请求返回 HTTP 200,证明无需仓库或包权限即可拉取。

2026-08-06 复核已将 `docker/login-action` 升级到声明 Node.js 24 runtime 的 4.6.0,并继续固定到已验证的完整 commit SHA。仓库内 `actions/checkout`、`actions/setup-go`、`actions/setup-node` 和 `docker/login-action` 的当前 pin 均声明 `node24`,原 Node.js 20 弃用提示已从配置源消除。


## 2026-10-18 发布前全面体检

范围为 `worktree/prism-ui` 在 `6be684f` 上的完整代码及本轮修复,不仅是 UI 分支差异。两位主代理同模型、两位 gpt-6.1-sol(max)分别审查安全边界、后端一致性、前端功能、UI/性能/发布配置,主代理复核与验收。没有增加功能、推送、打标签或发布。

### 已确认并修复

1. 迁移列表把磁盘名中的字面 `%20` 当 URL 转义,可能删除另一个带空格的文件。现先对真实路径编码,再应用已有路径校验;无法安全表示的文件计入跳过项。
2. 根路径 `/` 可创建映射却被管理入口重定向覆盖。现作为保留路径,创建和导入均拒绝。
3. portable 构建误要求 GIF 每帧覆盖整张画布。现按逻辑画布验证,接受合法局部帧,并保留完整画布乘帧数的像素预算。
4. 同路径别名创建、删除和导入的数据库/内存更新可能乱序。复用现有按键锁,只串行同路径修改;锁顺序为路径锁、重建屏障、数据库与索引,逆序释放。读交付不加锁,空闲锁项回收。
5. 修改密码时 `invalid_credentials` 误清有效会话。现仅此端点的凭据错误保留会话,`invalid_session` 仍退出。
6. 图库允许选中超过 API 上限的 100 张图片。现与迁移页一致限制每批选择,仍允许取消后替换选择。
7. PNG 压缩等级空字符串被转换为 0。现拒绝空白,合法的 0 继续接受。
8. 英文迁移页 768px 下标题工具行溢出。将该行的纵排与换行规则前移到 1023px,保持已有页面风格。

Go 更新到 1.26.9、x/image 更新到 0.45.0,解决实际调用链的标准库和 VP8L 解码公告;开发依赖仅更新兼容安全补丁。完整 `npm audit` 为 0;`govulncheck` 的实际调用链和已导入包漏洞均为 0,未调用模块子包的公告未误报成产品漏洞。

### 本轮验证

- 指定 Node 24.19.0 / Go 1.26.9 下 `make check` 通过:Go vet/tests、shell 语法、OpenAPI 类型一致性、lint、typecheck、58 项前端测试。
- Chromium 桌面与移动端 `make e2e`:36/36,包括新加入的英文 768px 深浅主题回归。
- 全部内部 Go 包竞态检测通过;锁修复完成后再次验证受影响的 7 个包。并发、重建、错误释放及已有图片串行化测试重复 20 次通过;临时移除锁后两个竞态回归按预期失败。
- 初轮 Chromium 布局检查为 7 页面 × 4 宽度 × 2 语言 × 2 主题,共 112 组合,发现并修复上述 768px 溢出。
- 独立 4 倍 CPU 节流导航:359 个帧间隔,median 16.7ms、p95 16.8ms、max 49.9ms,无超过 50ms 的帧,无 LongTask 记录。这是本机诊断,不作为跨设备性能承诺。
- 1 万/10 万路径索引基准查找失败均为 0,约 161.57/148.59 bytes/path,低于 1024 bytes/path 门槛。
- 原生 arm64 完整候选镜像构建、vips 测试和容器 smoke 通过。1 CPU、768MiB、并发 1 下 16 个 16MP 请求全部成功,busy 0,p95 3254.947ms,峰值 356995072 字节,低于 20s/512MiB 门槛。

101 张图片的真实组件回归保留分页、选择上限、取消/替换及实际请求 payload 断言,仅该条使用 15 秒超时以容纳并行容器构建的 CPU 竞争,没有放宽全局测试超时。

- 补充 WebKit 26.6:112 组布局和登录、上传、详情返回、移动导航、错误密码保留会话共 5 条交互全部通过,无页面脚本错误;不是 iPhone 真机 Safari 测试。
- amd64 候选在本机模拟架构下完成构建、vips 测试与完整 smoke;同等资源限制下 16/16 成功、busy 0、p95 2501.138ms、峰值 421814272 字节。模拟数据不代替正式发布流程的原生 amd64 runner 结果。
- 两个平台的 smoke 均覆盖精简非 root 镜像、只读目录错误、四种格式、转换、导入冲突、Range/ETag/HEAD、临时文件和资源回收、重启及 SIGTERM 正常退出。

以上是当前工作树的本地验收证据;正式标签发布后的原生双架构质量门和镜像/manifest 校验仍由现有发布流程执行,本轮没有提前执行发布。

## 2026-10-18 第二轮审查与修复

从 `a600f6d` 的完整 worktree 继续审查,仍由两位主代理同模型与两位 gpt-6.1-sol(max)分工,主代理复现、复核和验收。只修复有实际证据的问题,没有新增功能或发布。

1. 上轮禁止新建 `/` 映射后,历史数据库中已存在的 `/` 会导致索引加载失败,阻止启动。加载时仅跳过这一历史记录并计入跳过数,保留数据库记录供管理端删除;新增根映射和其他非法记录仍拒绝。
2. 合法局部首帧 GIF 的 portable 缩略图仍以子帧尺寸生成,丢失逻辑画布。现在保留画布、偏移、背景和透明区域,只解首帧,也不分配第二张全尺寸画布;测试覆盖 1024×512 缩放为 512×256。
3. Token 创建提交后、缓存发布前被吊销,迟到的缓存更新可能使已吊销 Token 再次有效。创建和吊销仅在写入链路串行,锁位于重建屏障之前,鉴权热路径不加锁。
4. 登录与资料修改交错可能发布旧资料会话;两个资料修改也可能把数据库和索引更新成不同版本。资料修改复用现有身份写锁,保持与登录和密码修改相同的锁顺序。
5. 登录成功后的两个重定向分支竞争,可能跳回上传页,Guard 还会丢失原查询参数。现在共用本地管理页目的地,并保留 pathname、search、hash;真实 AuthProvider/Router 回归覆盖重渲染。
6. 长合法历史路径创建成功后,提示文本撑宽移动页面。仅给成功提示增加任意位置换行,真实 API 创建回归覆盖中英文 393px。
7. Markdown 图片名只转义右方括号,`screen[1].png` 被 CommonMark 参考解析器解析成普通链接。现在同时转义左右方括号和反斜杠,不修改原文件名或其他链接格式。
8. 英文手机页面的文件选择按钮挤压说明文字成竖栏。窄屏将按钮移到独立一行,保留桌面布局,并检查文字宽度与选择区高度。
9. 完整 E2E 复现删除详情的迟到刷新再次导航:浏览器返回已经改变 URL,React 尚未提交新页面,原卸载检查仍放行。详情同步监听 `popstate` 使旧返回意图失效,保留已有 location key/卸载检查;不以延长等待或放宽测试掩盖问题。

第一项是上轮修复引入的历史数据兼容回归;第二项是上轮 GIF 修复未覆盖的缩略图链路。第九项源于本分支 `40f28d8` 恢复删除后的列表上下文时引入的异步返回流程;`cff8fae` 增加的保护未覆盖浏览器 POP 已发生、React 路由尚未提交的窗口,本轮由 `24aa435` 补齐。

第 3 至 8 项为本轮开始前已存在、在本轮复现的问题,不将其统一归为原始主分支的历史遗留。各项均保留对应回归覆盖。

### 第二轮验证

- 最终 `make check build` 通过:Go vet/tests、shell 语法、生成类型一致性、构建、lint、typecheck、68 项前端测试。
- `go test -race ./internal/...` 全部通过;资料并发、历史根映射及 GIF 回归重复 20 次通过。恢复旧实现的负向对照能复现原问题;Token 回归还直接核对数据库 revoked 和鉴权失败。
- 完整 E2E 首轮 36/37,实际捕获第九项;修复后原删除返回用例连续 10 次通过。组件回归固定浏览器 POP 与路由提交的窗口,禁用新监听后确定失败。
- 最终版本完整 Chromium 桌面与移动端 E2E 重新执行,37/37 通过(2.7 分钟)。
- WebKit 专项通过:真实创建长合法路径,中英文 393px 无横向溢出,英文文件说明宽度与高度合格,1440px 保持同排。此专项不等同于 iPhone 真机测试。
- 最终原生 arm64 生产镜像构建、vips 三包测试和完整容器 smoke 通过;覆盖登录、四种格式、转换、交付条件请求、别名导入、重建、重启与正常停止。专用容器、数据卷、临时候选镜像已清理。
- 本轮没有改变依赖、工具链或容器配置;未重复上一节的双架构性能压测和依赖扫描,上一节数字保留为上一轮证据。