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:
@@ -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:])
|
||||
}
|
||||
Reference in New Issue
Block a user