26.9(安全审计修复版)

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 7f060dd0e4
173 changed files with 32455 additions and 0 deletions
+103
View File
@@ -0,0 +1,103 @@
package storage
import (
"path"
"strings"
)
// ChunkDir 实现默认分片目录约定:<父目录>/chunks/<uploadID>。
// local/s3/webdav 三引擎共用,保持分片路径一致。
func ChunkDir(savePath, uploadID string) string {
dir := path.Dir(savePath)
name := path.Base(savePath)
// 防御:savePath 非法时仍返回明确结构,具体引擎再做安全校验
if name == "." || name == "/" {
name = "file"
}
return path.Join(dir, "chunks", uploadID) + "/" + name
}
// ChunkPartPath 分片对象完整路径(相对存储根)。
func ChunkPartPath(savePath, uploadID string, index int) string {
dir := path.Dir(savePath)
return path.Join(dir, "chunks", uploadID, itoa(index)+".part")
}
// SanitizePath 清理相对路径:统一斜杠、去首尾斜杠、拒绝 .. 穿越。
// 返回清理后的相对路径与是否合法。
func SanitizePath(p string) (string, bool) {
raw := strings.ReplaceAll(strings.TrimSpace(p), "\\", "/")
raw = strings.TrimPrefix(raw, "/")
if raw == "" {
return "", false
}
cleaned := path.Clean(raw)
if cleaned == ".." || strings.HasPrefix(cleaned, "../") || path.IsAbs(cleaned) {
return "", false
}
// 拒绝任何单独的 .. 段
for _, seg := range strings.Split(cleaned, "/") {
if seg == ".." {
return "", false
}
}
return cleaned, true
}
// SanitizeFileName 清理文件名:剥离路径、替换非法字符、限制长度。
// 对齐参考 core/utils.py 的 sanitize_filename。
func SanitizeFileName(name string) string {
// 剥离路径
if idx := strings.LastIndexAny(name, "/\\"); idx >= 0 {
name = name[idx+1:]
}
var b strings.Builder
for _, r := range name {
switch {
case r < 0x20 || r == 0x7f:
b.WriteByte('_')
case strings.ContainsRune(`\*?:"<>|`, r):
b.WriteByte('_')
case r == ' ':
b.WriteByte('_')
default:
b.WriteRune(r)
}
}
cleaned := b.String()
// 压缩连续下划线
for strings.Contains(cleaned, "__") {
cleaned = strings.ReplaceAll(cleaned, "__", "_")
}
cleaned = strings.Trim(cleaned, "._")
if cleaned == "" {
return "unnamed_file"
}
if len(cleaned) > 255 {
cleaned = cleaned[:255]
}
return cleaned
}
// itoa 小整数转字符串。
func itoa(n int) string {
if n == 0 {
return "0"
}
neg := n < 0
if neg {
n = -n
}
var buf [21]byte
i := len(buf)
for n > 0 {
i--
buf[i] = byte('0' + n%10)
n /= 10
}
if neg {
i--
buf[i] = '-'
}
return string(buf[i:])
}