FileCodeBox Go 重写版 v2.5.6(安全审计修复版)

Go 1.27.1 (Gin+GORM) + Vue 3 文件快传服务:

- 安全审计全部修复(docs/security-audit-2026-09-05.md):
  bcrypt 密码哈希与自动升级、presign 直传服务端大小/内容校验、
  全局请求体上限、依赖升级(govulncheck 0 命中)、janitor 后台清理、
  管理端审计动作落库、/admin CORS 收紧、通知内容白名单净化、
  会话默认 7 天、限流缓存故障降级、robots.txt 端点等
- 前端:取件链接复制修复(不再重复拼接提取码)、markdown 净化器加固
- Redis 支持库号(FCB_REDIS_DB / redis://…/db URL)
- 文档:docs/api/* 与 openapi.yaml 同步最新行为(robots.txt、
  提码 5 位起、chunk 32MiB 上限、admin 审计动作等)

验证:gofmt/go vet/go test 全绿;二进制端到端冒烟通过
This commit is contained in:
2026-09-05 04:22:41 +08:00
commit 9686fe887a
173 changed files with 32455 additions and 0 deletions
+186
View File
@@ -0,0 +1,186 @@
# FileCodeBox Go 重写版 · 安全审计报告
- 审计日期:2026-09-05
- 审计范围:`server/`Go 1.27.1 Gin+GORM 后端,56 个文件约 13,900 行)、`web/`Vue 3 前端)、`deploy/`Dockerfile / docker-compose
- 审计方式:人工代码审读(认证/会话、上传下载全链路、存储引擎、配置与注入面)+ 工具佐证(`go vet``govulncheck``npm audit`
- 结论速览:**未发现可直接导致 RCE、SQL 注入、路径穿越或认证绕过的高危问题**;发现 5 项中危问题(密码哈希强度、S3 直传校验缺口、请求体无上限 DoS、依赖漏洞、上传会话资源滥用)与若干低危/加固建议。
- **修复状态(2026-09-05 第二轮):M1M5、L1L10 及可行动 Info 项已全部修复**,逐项见各条目「✅ 修复」标记;验证:`gofmt`/`go vet`/`go test ./...` 全绿,`govulncheck` 0 命中,二进制端到端冒烟(初始化→登录→审计落库→限流锁定→XSS 净化→robots→分享取件)通过。前端 `markdown.ts` 净化器加固需重建前端产物(已重建 `web/dist``server/web/dist`)方可进入 go:embed 二进制。
---
## 一、中危(Medium
### M1 管理员密码哈希强度不足(单轮 SHA256+盐)✅ 已修复
- 位置:`server/internal/settings/password.go:15-21`
- 原状:`HashPassword` 生成 `sha256$salt$hash`,单轮 SHA256 + 16 字节随机盐。GPU 单卡对 SHA256 可达 10¹⁰ 次/秒,若数据库泄露(SQLite 文件/Postgres 备份),弱口令可被瞬间离线爆破。
- ✅ 修复:`HashPassword` 改为 **bcryptcost 12**,输出 `bcrypt$` + 原生 `$2a$12$…` 哈希串;`VerifyPassword` 兼容 bcrypt$/sha256$/明文三种历史格式实现平滑迁移;新增 `NeedsRehash`,管理员登录成功时自动将旧格式重哈希写回(`adminLogin` 内触发);密码长度 >72 字节按 bcrypt 语义截断(`bcryptBytes`);`GenerateJWTSecret` 的 rand 错误不再忽略(panic 显式失败)。测试:settings 包 + `TestAdminPasswordAutoUpgrade`
### M2 S3 预签名直传(direct 模式)绕过大小与类型校验 ✅ 已修复
- 位置:`server/internal/api/presign.go:59-171`init)、`presign.go:275-345`confirm)、`server/internal/storage/s3.go:538-554`PresignPutURL
- 原状:
- init 只校验**声明**的 `file_size`;预签名 PUT URL 未签入 Content-Length 约束,客户端实际可 PUT 任意大小对象;
- confirm 仅 `FileExists`,不 `Stat` 校验实际对象大小,分享记录 `Size` 直接取声明值;
- magic bytes / 类型白名单校验在 direct 模式完全不生效(内容不经过服务器)。
- 影响:开启游客上传 + S3 引擎的部署中,任意访客可绕过 `max_file_size``storageLimit`(配额按声明值记账),向桶内塞入任意大小/内容的对象(存储成本攻击、策略绕过)。
- ✅ 修复(多引擎一致,local/S3/WebDAV 全覆盖):
1. `Storage` 接口新增 `HeadMeta(ctx, savePath, headBytes)`S3 用 Range GET `bytes=0-(n-1)`WebDAV 用 Stat + Rangelocal 直接 Open
2. `presignConfirm` 现在:HeadMeta 取实际大小 → 超策略上限则 **DeleteFile + 释放配额 + 403**;实际大小与声明差 >1KB → **DeleteFile + 400**;前 64 字节补做 `validateFileMagic`
3. `presignInit` 拒绝 `file_size <= 0` 声明;
4. 测试:`TestPresignConfirmRejectsOversizeObject``TestPresignConfirmRejectsSizeMismatch`
### M3 请求体无全局大小上限,多处“先整读后校验”可被 DoS ✅ 已修复
- 位置:
- `server/internal/api/helpers.go:681-721``bindJSONOrForm` 对 text/plain 形态 `io.ReadAll` 全量进内存;JSON 绑定同样全量读入)
- `server/internal/api/chunk.go:314``io.ReadAll(LimitReader(f, ChunkSize+1))` 单分片全量进内存,`chunk_size` 上界=策略 `max_file_size`schema 允许至 10GiB
- `server/internal/api/share.go:100-128`(文本 222KB 限制在读完整个 body 之后才判定)
- 原状:全服务无 `http.MaxBytesReader`,也无上传前的 Content-Length 预检;multipart 大文件会先被完整解析(>32MB 落临时盘)后才被 `CheckSize` 拒绝。
- 影响:默认 `openUpload=1` 的部署下,未认证攻击者可用大 body 消耗内存/磁盘/带宽。
- ✅ 修复:
1. 新增 `middleware.BodyLimit(limitFn)``main.go` 全局装配(在 GuardNotInitialized 之后、Audit 之前):`/setup``/admin/*``/share/text|metadata|select` 一律 1MiB,其余端点 `maxFileSize(+2MiB 开销)`(前端单请求单分片,已核实安全);
2. `/share/text` 入口先查 `Content-Length > 441KB` 直接 403(读前预检);
3. 单分片 `chunk_size` 硬上限 32MiB(超出 400),不再跟随 10GiB 的文件策略;
4. 测试:`TestChunkSizeCap`
### M4 依赖漏洞(govulncheck 实际命中 3 个 + 8 个 imported 级)✅ 已修复
- 工具输出(`govulncheck ./...`):
- `golang.org/x/text v0.30.0`GO-2026-5970 非法输入死循环 DoS(经 gorm 归一化路径可达),修复于 **v0.39.0**
- `github.com/quic-go/quic-go v0.54.0`GO-2026-5676(修复 v0.59.1)、GO-2025-4233(修复 v0.57.0QPACK 扩张 DoS(实际未启用 HTTP/3 监听,实践影响低);
- `golang.org/x/net v0.45.0`8 个 imported 级漏洞(GO-2026-5030/5029/5028/5027/5026/5025/4918 等,含 HTTP/2 传输死循环),修复于 v0.53v0.55。
- ✅ 修复:`x/text v0.41.0``x/net v0.58.0``x/crypto v0.56.0`(转为直接依赖,供 bcrypt)、`quic-go v0.59.1``govulncheck ./...` 复扫 **0 个可达漏洞**(模块级仅剩 1 个未调用项)。前端 `npm audit` 仍报 2 个 moderate`vue-i18n → @intlify/core-base`,上游暂无修复版,保持关注升级)。
### M5 上传会话与容量预留可被滥用(init 不计数、无过期清理)✅ 已修复
- 位置:`server/internal/api/chunk.go:39-157`chunkInit 从不 `Limiter.Add`,上传限流仅在 complete/presign-init/shareFile 成功时计数)、`server/internal/api/helpers.go:380-436`(预留 TTLchunk 24h / presign 15min
- 原状:
- 游客可无限次 `POST /chunk/upload/init` 创建会话(每次写入 `upload_chunks` 行 + 24h 容量预留),无后台任务回收过期预留、未完成会话与孤儿分片对象(local `chunks/` 目录、S3 `*.part`);
- 若配置了 `storageLimit`,攻击者可用多次 init 把全部配额占用满 24 小时 → 全站上传 507(拒绝服务);默认 `storageLimit=0` 时则是磁盘/DB 垃圾持续累积。
- ✅ 修复:
1. `chunkInit` 在新会话保留成功后即 `Limiter.Add(c, LimitUpload)` 计数;
2. chunk 预留 TTL 24h → **2h**(续传刷新);
3. 新增 `internal/janitor` 后台清理循环(默认 10 分钟):过期 `storage_reservations`、超时(>24h 未完成)`upload_chunks` 会话(连带清理分片对象)、过期直传 presign 会话(连带删除残留对象);`main.go` 启动时随 ctx 拉起。
---
## 二、低危(Low
### L1 初始化向导(/setup)存在接管窗口 ✅ 已修复
- 位置:`server/internal/api/setup.go:47-63``middleware/audit.go:222-236`
- 原状:服务公开到公网后、管理员完成 /setup 前,任何人可抢先完成初始化并设置管理员密码(经典 setup race;双检查只防并发写坏,不防抢占)。
- ✅ 修复:新增 `autoInitIfNeeded``main.go`SystemStart 后执行)——设置 `FCB_ADMIN_PASSWORD`(≥8 位,否则告警跳过)即可在服务启动瞬间完成管理员初始化并生成 jwt_secret,消除 /setup 被抢占窗口;`deploy/.env.example` 与 README 安全清单已补充说明(初始化后建议移除该变量)。
### L2 下载令牌为非 HMAC 拼接哈希且非常量时间比较 ✅ 已修复
- 位置:`server/internal/api/helpers.go:249-253``sha256(code‖timeFactor‖"000"‖secret)`)、`share.go:458``key != GetSelectToken(...)`
- 原状:secret 后置拼接,长度扩展不适用、256 位密钥不可爆破,当前**不可实际利用**;但拼接串存在理论歧义(code 与时间窗数字边界重叠),且字符串比较非常量时间。
- ✅ 修复:`GetSelectToken` 改为 **HMAC-SHA256(secret, code‖timeFactor)**;新增 `VerifySelectToken``hmac.Equal` 常量时间比较(允许当前/上一两个时间窗,防临界失效);`shareDownload` 已切换到 `VerifySelectToken`
### L3 数字取件码空间过小,防撞库完全依赖单 IP 限流 ✅ 已修复
- 位置:`server/internal/api/helpers.go:114-125``validatePickupCode`4 位下限)
- 原状:`code_generate_type=number` 时仅 9 万空间(5 位数字),默认 `errorCount=10/分/IP` 下单 IP 需约 6 天扫完,分布式多 IP 可显著缩短;自定义码允许 4 位(36⁴≈168 万)。
- ✅ 修复:自定义提码最小长度 4 → **5 位**`pickupCodeMinLen=5`36⁵≈6000 万空间);测试 `v31_test.go``TestPickupCodeMinLen` 同步更新。
### L4 `enableChunk` 开关后端不强制 ✅ 已修复
- 位置:`server/internal/api/router.go:56-67`
- 原状:`/chunk/*` 路由不检查 `cfg.EnableChunk()`,关闭开关后接口仍可用(仅前端隐藏入口)。若该开关被当作安全策略,需在 handler 层强制(403)。
- ✅ 修复:新增 `requireChunkEnabled` 守卫(关闭时 403「分片上传未启用」),挂到 `chunkInit`(首检查)、`chunkUpload``chunkComplete` 三个端点;测试 `TestChunkToggleEnforced` 验证 0→403 / 1→200。
### L5 管理端安全事件未入审计日志 ✅ 已修复
- 位置:`server/internal/middleware/audit.go:55-78`DefaultClassifier 仅覆盖 upload/download
- 原状:管理员登录失败、配置修改、密码修改、引擎切换、文件删除等管理操作均不落审计。
- ✅ 修复:`DefaultClassifier` 扩展 `adminAuditActions`login/logout、config/update、settings/password、storage/switch、file/update/delete/batch-*、policy-action 等 POST/PATCH/DELETE 敏感操作 → `audit.ActionAdmin`);`Audit` 中间件跳过条件由「非 upload/download 即跳过」改为「分类未命中才跳过」,admin 动作同样建 `auditEntry` 并按 HTTP 状态兜底落库;新增 `audit_l5_test.go` 回归。端到端实测:登录失败 401 落 `admin/denied`、成功落 `admin/success`
### L6 CORS 对所有接口(含 /admin/*)放开 `*` ✅ 已修复
- 位置:`server/internal/middleware/cors.go:10`
- 原状:Bearer 模式下无 CSRF 风险,但一旦 token 泄露(见 L8),任意网站均可跨域携带 token 调用管理 API。
- ✅ 修复:`Cors` 重写——`/admin/*` 请求带 Origin 且既不同源也不在白名单(`site_domain` 配置注入)时**不回任何 CORS 头**(浏览器拦截跨域读取),预检直接 204;公开接口维持 `*`Bearer 认证,无 Cookie CSRF 面);无 Origin 的非浏览器请求不受影响。
### L7 管理端通知内容 `v-html` 直出(存储型 XSS 面)✅ 已修复
- 位置:`web/src/components/NotifyPop.vue:23``notify_content` 来自 `GET /api/v1/config`,管理端可设)
- 原状:设计上"允许 `<a>` 等受控 HTML",但服务端/前端均无净化。管理员账号被盗即可对全站访客注入脚本。
- ✅ 修复(双侧):
1. 服务端新增 `settings.SanitizeInlineHTML` 白名单净化器(`sanitize.go` + 18 个单测):仅保留文本与 `<a href="http(s)://|/|#">`(引号内 `>`、未闭合标签、`script/style/iframe/svg/math` 等危险标签连内容整体丢弃、事件属性不透传、`javascript:/data:` 拒绝);在 **adminConfigUpdate 写入侧****publicConfig 读取侧** 双重调用(覆盖历史存量与直改库数据);
2. 前端 `web/src/utils/markdown.ts` 净化器加固:黑名单补 `svg/math/frame/applet/template/noscript` 等,新增 `srcdoc/sandbox/formaction/action/xlink:href/srcset` 等危险属性表,URL 校验改为协议白名单(http/mailto/相对/锚点,src 另许 `data:image/`)。端到端实测:`<script>alert(1)</script>``javascript:` 链接被剔除、合法 `<a href="https://…">` 保留。
- 注:前端净化器改动需重建前端产物(已重建 `web/dist` 并同步 `server/web/dist`)方进入 go:embed 二进制;未来渲染任何外部内容前建议仍替换为 DOMPurify。
### L8 管理员令牌存 localStorage、默认会话 30 天 ✅ 已修复
- 位置:`web/src/api/http.ts:14-33``server/internal/config/config.go:110`
- 原状:XSS 可窃取且有效期长(可配 1–365 天)。
- ✅ 修复:`AdminSessionExpireDefault` 30 天 → **7 天**(仍可配 1–365 天,config 包测试通过);敏感操作改密已有旧密码校验 + jwt_secret 轮换(全部旧 token 失效)。`FCB_ADMIN_SESSION_EXPIRE` 环境变量种子同步支持(`main.go` + `.env.example` 注释)。
### L9 缓存故障时限流 fail-open ✅ 已修复
- 位置:`server/internal/middleware/ratelimit.go:148-164`
- 原状:Redis/缓存异常时 `Check` 一律放行(登录爆破防护随之失效)。
- ✅ 修复:`Check`/`Add` 区分 `cache.ErrNotFound`(窗口内无计数,正常放行)与真实缓存故障;故障时降级为**进程内固定窗口计数**(带过期清理与容量上限 8192,单实例语义),缓存恢复自动回到共享缓存;测试 `TestRateLimiterFallbackOnCacheFailure` 证明故障降级下超限后 fail-close 拒绝。端到端实测:连续错误密码 3 次 401 后第 4 次起 423 锁定。
### L10 反向代理场景的限流与审计 IP 失真 ✅ 已修复(文档 + 配置面)
- 位置:`server/internal/middleware/ratelimit.go:32-96``config.go:52`
- 原状:实现(仅信任 `FCB_TRUSTED_PROXIES` 命中的直连地址才采信 XFF)是正确的;但部署文档未强调反代后必须配置可信代理,否则全体用户共享代理 IP 的限流桶(互相误伤)且审计 IP 全是代理地址;反向配置错误则可伪造 XFF 绕过限流。
- ✅ 修复:`deploy/README.md` 新增「安全清单(生产部署必读)」7 条(可信代理、立即初始化/FCB_ADMIN_PASSWORD、修改组件默认凭据与端口发布、Postgres TLS、会话有效期、限流降级语义、内置清理任务);`deploy/.env.example``FCB_TRUSTED_PROXIES``FCB_ADMIN_PASSWORD``FCB_ADMIN_SESSION_EXPIRE` 补充注释说明,弱凭据处(MinIO/WebDAV/Postgres)加「仅限本机冒烟,生产必须修改」警示。
---
## 三、提示 / 信息(Info)修复情况
1. `version`/`storage_engine` 公开暴露(小信息收集面):**保留**(前端展示与诊断需要,风险极低)。
2. compose 弱凭据/端口发布:✅ 已在 `.env.example` 与 README 安全清单加警示(凭据为示例值,端口建议删除或绑 127.0.0.1)。
3. `robotsText` 无路由:✅ 已新增 `GET /robots.txt``router.go`,输出配置内容,text/plain);端到端实测 200 且内容生效。
4. `adminFileList` LIKE 未转义:✅ 已加 `escapeLike``\``\\``%``\%``_``\_`+ `ESCAPE '\'`admin-only,防御性修复)。
5. Postgres 并发配额超额记账:✅ `reserveStorage` 包事务并对 Postgres 加 `pg_advisory_xact_lock`"FCBQ" 键)串行化配额判定;SQLite 写串行不受影响。
6. `crypto/rand` 错误忽略:✅ `GenerateJWTSecret` 已改 panic 显式失败(见 M1);`generateCode`/`randomHex` 维持重试回退(非安全关键路径)。
---
## 四、确认到位的安全设计(无需修改)
- **JWT**HS256 + `WithValidMethods` 双重防算法混淆;密钥为 64 位 hex 随机;改密自动轮换(使全部旧会话失效);过期/签名校验完备。
- **SQL**:全部 GORM/占位符参数化;排序字段白名单(`normalizeSortBy`);无字符串拼接 SQL。
- **路径安全**`SanitizePath`/`SanitizeFileName` + `withinRoot` + `EvalSymlinks` 符号链接逃逸双重校验;S3 key、WebDAV 路径同样清洗;未发现穿越。
- **下载响应**:统一 `application/octet-stream` + `attachment` + RFC 5987 文件名编码,杜绝分享文件被当 HTML 渲染的存储型 XSS;取件文本以 `text/plain` 下发且前端 `<pre>{{ }}</pre>` 渲染。
- **认证/授权**`/admin/*` 全组 Bearer 鉴权;`/setup` 受 GuardNotInitialized 白名单约束;旧默认密码 `FileCodeBox2023` 视为未初始化;`verifyLegacyDefault`/密码比较用 `hmac.Equal`
- **上传策略**:大小上限五处口径一致(share/chunk-init/chunk-累计/presign/complete),magic bytes 防伪造,自定义码查重 + 唯一索引兜底并发。
- **限流**:取件错误/登录失败/上传/metadata 四类固定窗口;clientIP 仅信任显式配置的代理。
- **容量**:单条 INSERT..SELECT 原子预留判定,防超卖。
- **部署**:多阶段构建、非 rootuid 10001)运行、`.dockerignore` 合理、`.env` 未含真实密钥(当前也非 git 仓库;初始化 git 后应将 `deploy/.env` 加入忽略清单)。
---
## 五、修复优先级建议(原计划,已全部落实)
| 优先级 | 事项 | 状态 |
|---|---|---|
| 立即 | M4 依赖升级;M2 的 confirm 大小校验 | ✅ 已完成 |
| 短期 | M1 密码哈希迁移 bcryptM3 全局 MaxBytesReaderM5 init 计数 + 清理循环 | ✅ 已完成 |
| 计划 | L1–L10 按运营形态取舍 | ✅ 已全部修复 |
---
## 六、修复验证记录(2026-09-05 第二轮)
- **静态检查**`gofmt -l .` 无输出、`go vet ./...` 通过、`go build ./...` 通过。
- **单元/集成测试**`go test ./...` 全绿(api/audit/cache/config/database/middleware/settings/storage 全部 ok),新增修复回归测试:
- `settings/sanitize_test.go`L7 净化器 19 例)
- `middleware/audit_l5_test.go`L5 admin 审计落库)
- `middleware/ratelimit_fallback_test.go`L9 缓存故障降级 fail-close
- `api/security_fixes_test.go`L4 开关强制、M3 chunk 上限、L3 提码长度、M2 confirm 大小/超限、M1 密码迁移)
- **依赖扫描**`govulncheck ./...` 0 可达漏洞;`npm audit --omit=dev` 剩 2 moderatevue-i18n 上游未发布修复,跟踪中)。
- **端到端冒烟**(编译二进制 + SQLite 实跑):
1. `/setup` 初始化 200(M1 生效:DB 中 `admin_token = bcrypt$$2a$12$…`);
2. 错误密码登录 401 → 审计落 `admin/denied`;成功登录落 `admin/success`L5);
3. 连续 3 次错误后第 4 次起 423 锁定(L9 限流);
4. 配置写入 `<script>alert(1)</script>` + `javascript:` 链接 → `/api/v1/config` 输出已剔除、合法 `<a href>` 保留(L7);
5. `/robots.txt` 200 且内容生效(Info3);
6. 文本分享创建 + 取件下载全链路 200。
- **遗留跟踪**`vue-i18n` 上游修复版本发布后升级(当前 npm audit 的 2 个 moderate 均来源于此)。