security: 会话可撤销与认证面加固

- users 加 token_version(schema v4):改密/登出递增,该用户全部已发 access/refresh 立即作废;refresh 改轮换式换发新 cookie
- 邀请码改精确匹配 + 注册成功后消费(一码一用),修复子串匹配让任意后缀放行、可无限薅体验金
- 生产校验拦下 .env.example 占位密钥;OT_ENV 枚举校验(拼错不再静默跳过断言);生产强制 COOKIE_SECURE;非 development 拒绝默认密码播种管理员
- WebAuthn 会话启用过期校验 + 4096 容量上限(未认证 login/begin 可无限撑爆内存),begin/complete 挂登录级限速
- /api/v1 挂 1MB 请求体上限;login 先限 IP 桶再解析 body;全局安全响应头 + CSP
- 连接池配置(PG 30/SQLite 8);SQLite DSN 自动追加 WAL + busy_timeout(驱动只认 _pragma,旧注释的 _journal_mode 写法不生效)
This commit is contained in:
Sakurasan committed 2026-10-29 02:56:46 +08:00
1 parent d6db0f8162
commit e3ff0d3329
14 files changed
+392 -37

No files matched your search

+28 -1
View File
@@ -33,11 +33,19 @@ type Service struct {
sessions map[string]webauthn.SessionData // keyed by challenge
}
// maxSessions challenge 会话上限:login/begin 无需认证,不设上限的话每次调用
// 都会向 map 永久写入一条(只有完成认证才删),持续打可把进程内存打爆。
const maxSessions = 4096
func New(db *gorm.DB, cfg Config) (*Service, error) {
wa, err := webauthn.New(&webauthn.Config{
RPDisplayName: cfg.Name,
RPID: cfg.RPID,
RPOrigins: []string{cfg.Origin},
Timeouts: webauthn.TimeoutsConfig{
Login: webauthn.TimeoutConfig{Enforce: true, Timeout: 5 * time.Minute, TimeoutUVD: 5 * time.Minute},
Registration: webauthn.TimeoutConfig{Enforce: true, Timeout: 5 * time.Minute, TimeoutUVD: 5 * time.Minute},
},
})
if err != nil {
return nil, err
@@ -224,8 +232,27 @@ func (s *Service) updateCredential(userID uint64, cred *webauthn.Credential) err
func (s *Service) storeSession(session *webauthn.SessionData) {
s.mu.Lock()
defer s.mu.Unlock()
// 到容量上限先清一轮已过期会话;仍满则淘汰最早过期的(未过期说明正在被
// 正常使用,丢最不紧迫的)。配合 Enforce 过期校验,map 大小有硬上界。
if len(s.sessions) >= maxSessions {
now := time.Now()
var oldestKey string
var oldestExp time.Time
for k, v := range s.sessions {
if now.After(v.Expires) {
delete(s.sessions, k)
continue
}
if oldestKey == "" || v.Expires.Before(oldestExp) {
oldestKey, oldestExp = k, v.Expires
}
}
if len(s.sessions) >= maxSessions && oldestKey != "" {
delete(s.sessions, oldestKey)
}
}
s.sessions[session.Challenge] = *session
s.mu.Unlock()
}
func (s *Service) takeSession(challenge string) (webauthn.SessionData, bool) {