mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-15 23:22:59 +08:00
fix(security): 补齐审计日志脱敏缺口 + step-up 下载/取消体验修复
审计发现修复: 高危——审计日志请求体脱敏不全(audit_logs 沦为明文凭证聚合点): - 键名归一化比对(小写+去分隔符),覆盖 privateKey/apiv3key 等无分隔符与 camelCase 写法 - 程序化并入 SensitiveCredentialKeys 与 providerSensitiveConfigFields 两份权威敏感表,防清单漂移 - 补齐 proxy_key(内嵌代理密码)、custom_key(自设 API Key 明文)精确键 - Codex session 导入路由 body 整体由粘贴的 auth JSON 构成,键级脱敏无法覆盖,整体不入库 - 新增守卫测试:两份权威表的每个键必须被审计脱敏命中;provider_key 等渠道标识保留以便追责 中危——step-up 前端体验: - 备份下载改同页 anchor 导航(预签名 URL 后端强制 attachment disposition), 避免 step-up 弹窗 await 耗尽瞬态激活后 window.open 被浏览器拦截 - useStepUp.run 用户取消时抛 StepUpCancelledError sentinel, 三个调用点静默处理,不再把取消误报为红色错误 toast 低危: - 修正审计中间件挂载位置的陈旧注释(实际挂在认证之后) - 审计 body 捕获改 LimitReader 按 256KB 上限截断读取,超出部分拼接回填, 避免大体积导入请求被完整复制进内存两次 - TOTP step-up 弹窗验证成功后即时清空验证码输入
This commit is contained in:
@@ -78,9 +78,16 @@ var auditActionOverrides = map[string]string{
|
||||
"DELETE /api/v1/admin/settings/admin-api-key": "admin.admin_api_key.delete",
|
||||
}
|
||||
|
||||
// auditBodyOmittedRoutes 请求体几乎整体由凭证构成的路由(如整块粘贴 auth JSON 的导入接口)。
|
||||
// 这类 body 的凭证内嵌在普通字符串值里,键级脱敏无法覆盖,整体不入库。
|
||||
var auditBodyOmittedRoutes = map[string]struct{}{
|
||||
"POST /api/v1/admin/accounts/import/codex-session": {},
|
||||
}
|
||||
|
||||
// NewAuditLogMiddleware 创建审计中间件。
|
||||
// 记录范围:变更类请求(POST/PUT/PATCH/DELETE)+ 白名单内的敏感 GET 读取。
|
||||
// 该中间件应挂在认证中间件之前,以便同时记录认证失败的尝试。
|
||||
// 挂载位置:admin / user / admin-payment 组挂在各自认证中间件之后(只审计已认证请求,
|
||||
// 未过认证的 401/403 不入库);auth 组(登录/注册/刷新)无前置认证,天然记录失败尝试。
|
||||
func NewAuditLogMiddleware(auditService *service.AuditLogService) AuditLogMiddleware {
|
||||
return AuditLogMiddleware(func(c *gin.Context) {
|
||||
routeKey := c.Request.Method + " " + c.FullPath()
|
||||
@@ -105,11 +112,19 @@ func NewAuditLogMiddleware(auditService *service.AuditLogService) AuditLogMiddle
|
||||
}
|
||||
|
||||
// 捕获请求体(读出后回填,避免影响后续 ShouldBindJSON)。
|
||||
// 只读取脱敏解析上限内的字节,超出部分与已读部分拼接回填,
|
||||
// 避免大体积导入请求被完整复制进内存两次。
|
||||
var bodyRedacted string
|
||||
if c.Request.Body != nil && c.Request.Method != "GET" {
|
||||
raw, err := io.ReadAll(c.Request.Body)
|
||||
if _, omit := auditBodyOmittedRoutes[routeKey]; omit {
|
||||
bodyRedacted = "<credential-bearing body omitted>"
|
||||
} else if c.Request.Body != nil && c.Request.Method != "GET" {
|
||||
orig := c.Request.Body
|
||||
raw, err := io.ReadAll(io.LimitReader(orig, service.AuditRequestBodyCaptureLimit+1))
|
||||
if err == nil {
|
||||
c.Request.Body = io.NopCloser(bytes.NewReader(raw))
|
||||
c.Request.Body = &restoredBody{
|
||||
Reader: io.MultiReader(bytes.NewReader(raw), orig),
|
||||
closer: orig,
|
||||
}
|
||||
bodyRedacted = service.RedactAuditBody(raw, c.GetHeader("Content-Type"))
|
||||
}
|
||||
}
|
||||
@@ -199,6 +214,15 @@ func NewAuditLogMiddleware(auditService *service.AuditLogService) AuditLogMiddle
|
||||
})
|
||||
}
|
||||
|
||||
// restoredBody 把审计中间件按上限读出的前缀与未读完的原始 body 拼接回填,
|
||||
// 保证 handler 读到完整请求体;Close 委托给原始 body。
|
||||
type restoredBody struct {
|
||||
io.Reader
|
||||
closer io.Closer
|
||||
}
|
||||
|
||||
func (b *restoredBody) Close() error { return b.closer.Close() }
|
||||
|
||||
// MaskedRequestCredential 提取请求头中的凭证并做首尾掩码。
|
||||
func MaskedRequestCredential(c *gin.Context) string {
|
||||
if apiKey := strings.TrimSpace(c.GetHeader("x-api-key")); apiKey != "" {
|
||||
|
||||
Reference in New Issue
Block a user