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,118 @@
package handler
import (
"bytes"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"lijiaoqiao/gateway/internal/router"
)
func TestMED05_RequestBodySizeLimit(t *testing.T) {
// MED-05: Request body size should be limited to prevent DoS attacks
// json.Decoder should use MaxBytes to limit request body size
r := router.NewRouter(router.StrategyLatency)
h := NewHandler(r)
// Create a very large request body (exceeds 1MB limit)
largeContent := strings.Repeat("a", 2*1024*1024) // 2MB
largeBody := `{"model": "gpt-4", "messages": [{"role": "user", "content": "` + largeContent + `"}]}`
req := httptest.NewRequest("POST", "/v1/chat/completions", bytes.NewBufferString(largeBody))
req.Header.Set("Content-Type", "application/json")
rr := httptest.NewRecorder()
h.ChatCompletionsHandle(rr, req)
// After fix: should return 413 Request Entity Too Large
if rr.Code != http.StatusRequestEntityTooLarge {
t.Errorf("expected status 413 for large request body, got %d", rr.Code)
}
}
func TestMED05_NormalRequestShouldPass(t *testing.T) {
// Normal requests should still work
r := router.NewRouter(router.StrategyLatency)
prov := &mockProvider{name: "test", models: []string{"gpt-4"}, healthy: true}
r.RegisterProvider("test", prov)
h := NewHandler(r)
body := `{"model": "gpt-4", "messages": [{"role": "user", "content": "hello"}]}`
req := httptest.NewRequest("POST", "/v1/chat/completions", bytes.NewBufferString(body))
req.Header.Set("Content-Type", "application/json")
rr := httptest.NewRecorder()
h.ChatCompletionsHandle(rr, req)
// Should succeed (status 200)
if rr.Code != http.StatusOK {
t.Errorf("expected status 200 for normal request, got %d", rr.Code)
}
}
func TestMED05_EmptyBodyShouldFail(t *testing.T) {
// Empty request body should fail
r := router.NewRouter(router.StrategyLatency)
h := NewHandler(r)
req := httptest.NewRequest("POST", "/v1/chat/completions", bytes.NewBufferString(""))
req.Header.Set("Content-Type", "application/json")
rr := httptest.NewRecorder()
h.ChatCompletionsHandle(rr, req)
// Should fail with 400 Bad Request
if rr.Code != http.StatusBadRequest {
t.Errorf("expected status 400 for empty body, got %d", rr.Code)
}
}
func TestMED05_InvalidJSONShouldFail(t *testing.T) {
// Invalid JSON should fail
r := router.NewRouter(router.StrategyLatency)
h := NewHandler(r)
body := `{invalid json}`
req := httptest.NewRequest("POST", "/v1/chat/completions", bytes.NewBufferString(body))
req.Header.Set("Content-Type", "application/json")
rr := httptest.NewRecorder()
h.ChatCompletionsHandle(rr, req)
// Should fail with 400 Bad Request
if rr.Code != http.StatusBadRequest {
t.Errorf("expected status 400 for invalid JSON, got %d", rr.Code)
}
}
// TestMaxBytesReaderWrapper tests the MaxBytes reader wrapper behavior
func TestMaxBytesReaderWrapper(t *testing.T) {
// Test that limiting reader works correctly
content := "hello world"
limitedReader := io.LimitReader(bytes.NewReader([]byte(content)), 5)
buf := make([]byte, 20)
n, err := limitedReader.Read(buf)
// Should only read 5 bytes
if n != 5 {
t.Errorf("expected to read 5 bytes, got %d", n)
}
if err != nil && err != io.EOF {
t.Errorf("expected no error or EOF, got %v", err)
}
// Reading again should return 0 with EOF
n2, err2 := limitedReader.Read(buf)
if n2 != 0 {
t.Errorf("expected 0 bytes on second read, got %d", n2)
}
if err2 != io.EOF {
t.Errorf("expected EOF on second read, got %v", err2)
}
}