fix(security): 修复多个MED安全问题

MED-03: 数据库密码明文配置
- 在 gateway/internal/config/config.go 中添加 AES-GCM 加密支持
- 添加 EncryptedPassword 字段和 GetPassword() 方法
- 支持密码加密存储和解密获取

MED-04: 审计日志Route字段未验证
- 在 supply-api/internal/middleware/auth.go 中添加 sanitizeRoute() 函数
- 防止路径遍历攻击(.., ./, \ 等)
- 防止 null 字节和换行符注入

MED-05: 请求体大小无限制
- 在 gateway/internal/handler/handler.go 中添加 MaxRequestBytes 限制(1MB)
- 添加 maxBytesReader 包装器
- 添加 COMMON_REQUEST_TOO_LARGE 错误码

MED-08: 缺少CORS配置
- 创建 gateway/internal/middleware/cors.go CORS 中间件
- 支持来源域名白名单、通配符子域名
- 支持预检请求处理和凭证配置

MED-09: 错误信息泄露内部细节
- 添加测试验证 JWT 错误消息不包含敏感信息
- 当前实现已正确返回安全错误消息

MED-10: 数据库凭证日志泄露风险
- 在 gateway/cmd/gateway/main.go 中使用 GetPassword() 代替 Password
- 避免 DSN 中明文密码被记录

MED-11: 缺少Token刷新机制
- 当前 verifyToken() 已正确验证 token 过期时间
- Token 刷新需要额外的 refresh token 基础设施

MED-12: 缺少暴力破解保护
- 添加 BruteForceProtection 结构体
- 支持最大尝试次数和锁定时长配置
- 在 TokenVerifyMiddleware 中集成暴力破解保护
This commit is contained in:
Your Name
2026-04-03 09:51:39 +08:00
parent b2d32be14f
commit d44e9966e0
11 changed files with 1172 additions and 143 deletions

View File

@@ -0,0 +1,113 @@
package middleware
import (
"net/http"
"strings"
)
// CORSConfig CORS配置
type CORSConfig struct {
AllowOrigins []string // 允许的来源域名
AllowMethods []string // 允许的HTTP方法
AllowHeaders []string // 允许的请求头
ExposeHeaders []string // 允许暴露给客户端的响应头
AllowCredentials bool // 是否允许携带凭证
MaxAge int // 预检请求缓存时间(秒)
}
// DefaultCORSConfig 返回默认CORS配置
func DefaultCORSConfig() CORSConfig {
return CORSConfig{
AllowOrigins: []string{"*"}, // 生产环境应限制具体域名
AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowHeaders: []string{"Authorization", "Content-Type", "X-Request-ID", "X-Request-Key"},
ExposeHeaders: []string{"X-Request-ID"},
AllowCredentials: false,
MaxAge: 86400, // 24小时
}
}
// CORSMiddleware 创建CORS中间件
func CORSMiddleware(config CORSConfig) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 处理CORS预检请求
if r.Method == http.MethodOptions {
handleCORSPreflight(w, r, config)
return
}
// 处理实际请求的CORS头
setCORSHeaders(w, r, config)
next.ServeHTTP(w, r)
})
}
}
// handleCORS Preflight 处理预检请求
func handleCORSPreflight(w http.ResponseWriter, r *http.Request, config CORSConfig) {
func handleCORS Preflight(w http.ResponseWriter, r *http.Request, config CORSConfig) {
origin := r.Header.Get("Origin")
// 检查origin是否被允许
if !isOriginAllowed(origin, config.AllowOrigins) {
w.WriteHeader(http.StatusForbidden)
return
}
// 设置预检响应头
w.Header().Set("Access-Control-Allow-Origin", origin)
w.Header().Set("Access-Control-Allow-Methods", strings.Join(config.AllowMethods, ", "))
w.Header().Set("Access-Control-Allow-Headers", strings.Join(config.AllowHeaders, ", "))
w.Header().Set("Access-Control-Max-Age", string(rune(config.MaxAge)))
if config.AllowCredentials {
w.Header().Set("Access-Control-Allow-Credentials", "true")
}
w.WriteHeader(http.StatusNoContent)
}
// setCORSHeaders 设置实际请求的CORS响应头
func setCORSHeaders(w http.ResponseWriter, r *http.Request, config CORSConfig) {
origin := r.Header.Get("Origin")
// 检查origin是否被允许
if !isOriginAllowed(origin, config.AllowOrigins) {
return
}
w.Header().Set("Access-Control-Allow-Origin", origin)
if len(config.ExposeHeaders) > 0 {
w.Header().Set("Access-Control-Expose-Headers", strings.Join(config.ExposeHeaders, ", "))
}
if config.AllowCredentials {
w.Header().Set("Access-Control-Allow-Credentials", "true")
}
}
// isOriginAllowed 检查origin是否在允许列表中
func isOriginAllowed(origin string, allowedOrigins []string) bool {
if origin == "" {
return false
}
for _, allowed := range allowedOrigins {
if allowed == "*" {
return true
}
if strings.EqualFold(allowed, origin) {
return true
}
// 支持通配符子域名 *.example.com
if strings.HasPrefix(allowed, "*.") {
domain := allowed[2:]
if strings.HasSuffix(origin, domain) {
return true
}
}
}
return false
}

View File

@@ -0,0 +1,172 @@
package middleware
import (
"net/http"
"net/http/httptest"
"testing"
)
func TestCORSMiddleware_PreflightRequest(t *testing.T) {
config := DefaultCORSConfig()
config.AllowOrigins = []string{"https://example.com"}
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
corsHandler := CORSMiddleware(config)(handler)
// 模拟OPTIONS预检请求
req := httptest.NewRequest("OPTIONS", "/v1/chat/completions", nil)
req.Header.Set("Origin", "https://example.com")
req.Header.Set("Access-Control-Request-Method", "POST")
req.Header.Set("Access-Control-Request-Headers", "Authorization, Content-Type")
w := httptest.NewRecorder()
corsHandler.ServeHTTP(w, req)
// 预检请求应返回204 No Content
if w.Code != http.StatusNoContent {
t.Errorf("expected status 204 for preflight, got %d", w.Code)
}
// 检查CORS响应头
if w.Header().Get("Access-Control-Allow-Origin") != "https://example.com" {
t.Errorf("expected Access-Control-Allow-Origin to be 'https://example.com', got '%s'", w.Header().Get("Access-Control-Allow-Origin"))
}
if w.Header().Get("Access-Control-Allow-Methods") == "" {
t.Error("expected Access-Control-Allow-Methods to be set")
}
}
func TestCORSMiddleware_ActualRequest(t *testing.T) {
config := DefaultCORSConfig()
config.AllowOrigins = []string{"https://example.com"}
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
corsHandler := CORSMiddleware(config)(handler)
// 模拟实际请求
req := httptest.NewRequest("POST", "/v1/chat/completions", nil)
req.Header.Set("Origin", "https://example.com")
w := httptest.NewRecorder()
corsHandler.ServeHTTP(w, req)
// 正常请求应通过到handler
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d", w.Code)
}
// 检查CORS响应头
if w.Header().Get("Access-Control-Allow-Origin") != "https://example.com" {
t.Errorf("expected Access-Control-Allow-Origin to be 'https://example.com', got '%s'", w.Header().Get("Access-Control-Allow-Origin"))
}
}
func TestCORSMiddleware_DisallowedOrigin(t *testing.T) {
config := DefaultCORSConfig()
config.AllowOrigins = []string{"https://allowed.com"}
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
corsHandler := CORSMiddleware(config)(handler)
// 模拟来自未允许域名的请求
req := httptest.NewRequest("POST", "/v1/chat/completions", nil)
req.Header.Set("Origin", "https://malicious.com")
w := httptest.NewRecorder()
corsHandler.ServeHTTP(w, req)
// 预检请求应返回403 Forbidden
if w.Code != http.StatusForbidden {
t.Errorf("expected status 403 for disallowed origin, got %d", w.Code)
}
}
func TestCORSMiddleware_WildcardOrigin(t *testing.T) {
config := DefaultCORSConfig()
config.AllowOrigins = []string{"*"} // 允许所有来源
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
corsHandler := CORSMiddleware(config)(handler)
// 模拟请求
req := httptest.NewRequest("POST", "/v1/chat/completions", nil)
req.Header.Set("Origin", "https://any-domain.com")
w := httptest.NewRecorder()
corsHandler.ServeHTTP(w, req)
// 应该允许
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d", w.Code)
}
}
func TestCORSMiddleware_SubdomainWildcard(t *testing.T) {
config := DefaultCORSConfig()
config.AllowOrigins = []string{"*.example.com"}
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
corsHandler := CORSMiddleware(config)(handler)
// 测试子域名
tests := []struct {
origin string
shouldAllow bool
}{
{"https://app.example.com", true},
{"https://api.example.com", true},
{"https://example.com", true},
{"https://malicious.com", false},
}
for _, tt := range tests {
req := httptest.NewRequest("POST", "/v1/chat/completions", nil)
req.Header.Set("Origin", tt.origin)
w := httptest.NewRecorder()
corsHandler.ServeHTTP(w, req)
if tt.shouldAllow && w.Code != http.StatusOK {
t.Errorf("origin %s should be allowed, got status %d", tt.origin, w.Code)
}
if !tt.shouldAllow && w.Code != http.StatusForbidden {
t.Errorf("origin %s should be forbidden, got status %d", tt.origin, w.Code)
}
}
}
func TestMED08_CORSConfigurationExists(t *testing.T) {
// MED-08: 验证CORS配置存在且可用
config := DefaultCORSConfig()
// 验证默认配置包含必要的设置
if len(config.AllowMethods) == 0 {
t.Error("default CORS config should have AllowMethods")
}
if len(config.AllowHeaders) == 0 {
t.Error("default CORS config should have AllowHeaders")
}
// 验证CORS中间件函数存在
corsMiddleware := CORSMiddleware(config)
if corsMiddleware == nil {
t.Error("CORSMiddleware should return a valid middleware function")
}
}