security: remediate review B findings
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
# 审核专家 B:CHFS-NG 代码安全审计报告
|
||||
|
||||
- **审核人**:审核专家 B
|
||||
- **日期**:2026-10-02
|
||||
- **对象**:`/root/chfs-ng-research`(Go,11 个非测试源文件,约 3900 行)
|
||||
- **生产环境**:`chfs-ng.service`(root 用户),`bin/chfs-ng -file chfs.ini`,监听 *:18999
|
||||
- **验证**:`go vet ./...` 干净;`go test ./...` 全通过
|
||||
- **结论**:**有条件通过** —— 代码层面的路径防御与权限检查扎实,但生产部署配置存在阻断项,另有 2 高/3 中需处理
|
||||
|
||||
---
|
||||
|
||||
## 【阻断】生产配置使用弱密码 admin123,明文存储且文件全局可读
|
||||
|
||||
**【严重度:阻断】**
|
||||
|
||||
**【位置】** `/root/chfs-ng-research/chfs.ini`(生产配置文件)
|
||||
|
||||
**【问题描述】**
|
||||
生产 `chfs.ini` 中 `[admin]` 的密码为明文 `admin123`,文件权限为 `-rw-r--r--`(全局可读)。
|
||||
服务监听 `*:18999`(所有网卡),主机防火墙 INPUT 策略为 ACCEPT 且无规则,等同于直接暴露公网。
|
||||
远程攻击者无需任何前置条件,直接用 `admin/admin123` 登录即获得全部文件读写删除权限。
|
||||
|
||||
**【利用/触发条件】**
|
||||
`curl -X POST http://<host>:18999/chfs/session -d "user=admin&pwd=admin123"` 即得管理员 JWT。
|
||||
|
||||
**【修复建议】**
|
||||
1. 立即更换高强度密码;2. `chmod 600 chfs.ini`;3. 配置 `allow` IP 白名单或置于反向代理后;
|
||||
4. 考虑主机防火墙限制 18999 入站。
|
||||
|
||||
---
|
||||
|
||||
## 【高】/chfs/exist 未鉴权的文件存在性预言机
|
||||
|
||||
**【严重度:高】**
|
||||
|
||||
**【位置】** `internal/api/server.go:512` `func (s *Server) exist`
|
||||
|
||||
**【问题描述】**
|
||||
该端点全程未调用 `account(r, s)`,未做任何权限检查:`GET /chfs/exist?file=<path>` 返回 201/404
|
||||
直接泄露任意路径是否存在。所有其他文件端点都有 `MaskFor` 检查,唯独此处遗漏,
|
||||
与 README 宣称的"完善的权限体系"不一致。
|
||||
|
||||
**【利用/触发条件】**
|
||||
当前生产 guest=r,攻击者本就可列目录,实际影响有限;但在 guest 被收紧的部署中,
|
||||
该端点绕过权限模型造成信息泄漏。
|
||||
|
||||
**【修复建议】**
|
||||
加上 `if !account(r, s).MaskFor(p).HasRead() { 403 }`,与其他端点保持一致。
|
||||
|
||||
---
|
||||
|
||||
## 【高】缩略图接口无文件大小限制,可被解压炸弹打 OOM
|
||||
|
||||
**【严重度:高】**
|
||||
|
||||
**【位置】** `internal/api/server.go:1164` `func (s *Server) thumbnail`
|
||||
|
||||
**【问题描述】**
|
||||
`image.Decode(f)` 会把整个文件读入内存解码,而此前没有任何文件大小检查。
|
||||
缓存层虽有 8MB/64 项上限(`thumbnail_cache.go`),但 decode 本身在缓存之外无限制。
|
||||
攻击者(生产环境 guest 即有 read 权限)可请求超大文件或特制解压炸弹
|
||||
(小文件声明超大宽高,如 PNG 头声明 100000x100000 → 数十 GB 内存分配),直接 OOM 杀死服务。
|
||||
|
||||
**【利用/触发条件】**
|
||||
`GET /chfs/thumbnail?file=<大文件或炸弹图片>`,无需登录(guest 可读)。
|
||||
|
||||
**【修复建议】**
|
||||
`Stat` 后检查 `fi.Size()`,超过阈值(如 32MB)直接拒绝;或对 decode 加内存上限。
|
||||
|
||||
---
|
||||
|
||||
## 【中】目录 ZIP 下载无总大小/数量限制,CPU 与带宽耗尽
|
||||
|
||||
**【严重度:中】**
|
||||
|
||||
**【位置】** `internal/api/server.go:1290` `downloadSelectedZip`、`1368` `downloadDir`
|
||||
|
||||
**【问题描述】**
|
||||
两个接口把目录树流式打包为 zip 直接写回 response,全程无总字节数、无文件数上限
|
||||
(`downloadSelectedZip` 仅限制了顶层 path 参数 ≤100 个,但每个可为超大目录;`downloadDir` 无任何限制)。
|
||||
`zip.Deflate` 是 CPU 密集型,多并发请求可打满 CPU 并占满带宽。guest 可读即触发。
|
||||
|
||||
**【修复建议】**
|
||||
增加可配置的打包上限(总大小、文件数),超限返回 413;或对该接口加并发信号量。
|
||||
|
||||
---
|
||||
|
||||
## 【中】上传无单文件/总量配额,可填满磁盘
|
||||
|
||||
**【严重度:中】**
|
||||
|
||||
**【位置】** `internal/api/server.go:838` `upload`、`937` `upload2`、`savePart`
|
||||
|
||||
**【问题描述】**
|
||||
`ParseMultipartForm` 仅限制内存部分(64MB/128MB),`savePart` 的 `io.Copy(f, in)` 无任何大小上限。
|
||||
有写权限的用户可上传超大文件填满磁盘。当前生产仅 admin 有写权限,实际影响有限,
|
||||
但项目主打"动态权限控制"(会给普通用户开写权限),属于设计缺口。
|
||||
|
||||
**【修复建议】**
|
||||
增加 `upload.max-size` 配置项,`savePart` 中用 `io.LimitReader` 截断并报错。
|
||||
|
||||
---
|
||||
|
||||
## 【中】默认删除进系统回收站且永不清理,磁盘被"已删除"文件占满
|
||||
|
||||
**【严重度:中】**
|
||||
|
||||
**【位置】** `internal/storage/storage.go` `Trash`;`internal/config/config.go` 默认 `FileRemove: 1`
|
||||
|
||||
**【问题描述】**
|
||||
默认 `file.remove=1` 走 `wastebasket.Trash`(`~/.local/share/Trash`),该库无自动清理机制
|
||||
(只有模式 2 的 `MoveToTrash` 有 30 天清理)。长期运行的服务删除的文件永久占用磁盘,
|
||||
`df` 与用户认知不一致,最终表现为神秘的磁盘占满。
|
||||
|
||||
**【修复建议】**
|
||||
对模式 1 也加定期清理(如 30 天),或在文档中明确说明并给出清理方式。
|
||||
|
||||
---
|
||||
|
||||
## 【中低】search 无结果数量上限
|
||||
|
||||
**【严重度:中低】**
|
||||
|
||||
**【位置】** `internal/api/server.go:1009` `func (s *Server) search`
|
||||
|
||||
**【问题描述】**
|
||||
全树递归遍历,结果集无上限一次性 JSON 返回。超大目录树下内存与响应体膨胀,
|
||||
guest 可直接触发。
|
||||
|
||||
**【修复建议】**
|
||||
加 `limit` 参数(默认如 500),超限截断并提示。
|
||||
|
||||
---
|
||||
|
||||
## 【低】ipFilter 不识别 X-Forwarded-For,反代后 allow 白名单失效
|
||||
|
||||
**【严重度:低】**
|
||||
|
||||
**【位置】** `internal/api/server.go:2065` `ipFilter` / `clientIP`
|
||||
|
||||
**【问题描述】**
|
||||
`clientIP` 只读 `r.RemoteAddr`。README 建议生产用 HTTPS 反向代理,
|
||||
一旦接入反代,`allow` 看到的永远是代理 IP,白名单名存实亡(要么全放行要么全拒绝)。
|
||||
|
||||
**【修复建议】**
|
||||
支持可信代理 + `X-Forwarded-For` 解析,或在文档中明确说明该限制。
|
||||
|
||||
---
|
||||
|
||||
## 【低】密码哈希为无盐 SHA256
|
||||
|
||||
**【严重度:低】**
|
||||
|
||||
**【位置】** `internal/auth/auth.go` `func PasswordHash`
|
||||
|
||||
**【问题描述】**
|
||||
`users.json` 中存储 `hex(sha256(password))`,无盐。文件泄漏后彩虹表可直接破解弱密码。
|
||||
|
||||
**【修复建议】**
|
||||
改用 bcrypt(`golang.org/x/crypto/bcrypt`),并做旧哈希的兼容迁移。
|
||||
|
||||
---
|
||||
|
||||
## 已验证为安全的关键点(正面结论)
|
||||
|
||||
1. **JWT 密钥管理正确**:`main.go` 在未配置 `jwt.secret` 时生成随机 32 字节密钥并以 0600 权限持久化
|
||||
到 `users.json.jwt-secret`(生产已生效,文件存在)。`auth.go` 中的硬编码常量在 server 路径中不可达。
|
||||
2. **路径穿越防御扎实**:`unsafePath`(NUL + `..` 段)与 `storage.real()` 双层防御;`real()` 对最近存在祖先
|
||||
做 `EvalSymlinks`,连路径中间的符号链接逃逸都能拦住。多根目录经 `Multi` 路由后仍走同一检查。
|
||||
3. **权限检查完整**:除 `exist` 外所有 mutating 端点都有 `MaskFor` 检查;`rename` 为 create-only;
|
||||
`remove`/`transfer` 有递归 preflight;WebDAV 有独立 `webdavAuthFS` 包装,Move/Copy/RemoveAll 均有 `checkTree`。
|
||||
4. **上传 preflight**:`upload`/`upload2` 全量校验后再落盘,失败回滚已保存分片;`createExclusive` 防并发覆盖。
|
||||
5. **Cookie 安全**:JWT cookie 为 `HttpOnly`;生产 guest 为只读。
|
||||
6. **生产密钥文件权限正确**:`users.json.jwt-secret` 为 0600。
|
||||
|
||||
---
|
||||
|
||||
## 总体结论
|
||||
|
||||
**有条件通过**。
|
||||
|
||||
代码本身质量不错:路径穿越防御、权限模型、并发处理都经过认真设计,测试全过、vet 干净。
|
||||
真正的风险在**生产部署**:`admin123` 弱密码 + 公网直连 + 无防火墙是阻断项,必须立即处理。
|
||||
代码层面需修:`exist` 鉴权缺失(高)、缩略图 OOM(高),以及 ZIP/上传/回收站/搜索的资源限制(中)。
|
||||
修完阻断项和高优先级后可认为达到生产安全标准。
|
||||
Reference in New Issue
Block a user