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
+98
View File
@@ -0,0 +1,98 @@
// Package settings 密码哈希与校验:
// 新密码使用 bcrypt(格式 bcrypt$<bcrypt原生哈希串>);同时兼容两代旧格式——
// sha256$salt$hash(上一版)与旧版明文(迁移校验)。
// 安全审计 M1:单轮 SHA256+盐抗 GPU 爆破不足,新哈希统一升级 bcrypt。
// 兼容策略:VerifyPassword 支持全部三代格式;调用方可用 NeedsRehash 判定
// 登录成功后是否需要用新算法重哈希写回(登录升级路径见 api.adminLogin)。
package settings
import (
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"strconv"
"strings"
"golang.org/x/crypto/bcrypt"
)
// bcryptCost bcrypt 工作因子:122026 年桌面 CPU 单次校验约 100-250ms
// 离线爆破成本相比单轮 SHA256 提升数个数量级)。
const bcryptCost = 12
// bcryptMaxLen bcrypt 算法只取前 72 字节;超长输入统一截断,
// 避免 GenerateFromPassword/CompareHashAndPassword 对 >72 字节返回错误。
const bcryptMaxLen = 72
func bcryptBytes(password string) []byte {
b := []byte(password)
if len(b) > bcryptMaxLen {
b = b[:bcryptMaxLen]
}
return b
}
// HashPassword 生成 bcrypt$<hash> 格式密码哈希(<hash> 为 bcrypt 原生串
// `$2a$<cost>$<salt><hash>`cost 内嵌于哈希串中)。
func HashPassword(password string) string {
sum, err := bcrypt.GenerateFromPassword(bcryptBytes(password), bcryptCost)
if err != nil {
// 截断后仅剩非法 cost 等实现级错误:确定性失败优于弱哈希回落
panic("settings: bcrypt 哈希失败: " + err.Error())
}
return "bcrypt$" + string(sum)
}
// VerifyPassword 校验密码:支持 bcrypt$、sha256$salt$hash 与旧版明文三种格式。
func VerifyPassword(password, hashed string) bool {
if hashed == "" {
return false
}
switch {
case strings.HasPrefix(hashed, "bcrypt$"):
return bcrypt.CompareHashAndPassword([]byte(hashed[len("bcrypt$"):]), bcryptBytes(password)) == nil
case strings.HasPrefix(hashed, "sha256$"):
parts := strings.Split(hashed, "$")
if len(parts) != 3 {
return false
}
salt, stored := parts[1], parts[2]
sum := sha256.Sum256([]byte(salt + password))
return hmac.Equal([]byte(hex.EncodeToString(sum[:])), []byte(stored))
}
// 旧版明文比较(兼容迁移)
return hmac.Equal([]byte(password), []byte(hashed))
}
// NeedsRehash 判断哈希是否需要升级为当前算法/成本(登录成功后判定,透明迁移)。
// sha256 与明文一律 truebcrypt 成本低于当前 bcryptCost 时 true。
func NeedsRehash(hashed string) bool {
if !strings.HasPrefix(hashed, "bcrypt$") {
return true
}
// bcrypt 原生串格式:$2a$<cost>$<salt><hash>
parts := strings.Split(hashed[len("bcrypt$"):], "$")
if len(parts) < 4 {
return true
}
cost, err := strconv.Atoi(parts[2])
if err != nil {
return true
}
return cost < bcryptCost
}
// IsPasswordHashed 判断是否为受支持的哈希格式(bcrypt / sha256)。
func IsPasswordHashed(s string) bool {
return strings.HasPrefix(s, "bcrypt$") || strings.HasPrefix(s, "sha256$")
}
// GenerateJWTSecret 生成 64 字符十六进制随机密钥。
func GenerateJWTSecret() string {
b := make([]byte, 32)
if _, err := rand.Read(b); err != nil {
panic("settings: crypto/rand 不可用: " + err.Error())
}
return hex.EncodeToString(b)
}