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 中集成暴力破解保护
221 lines
5.7 KiB
Go
221 lines
5.7 KiB
Go
package middleware
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/golang-jwt/jwt/v5"
|
|
)
|
|
|
|
// TestMED09_ErrorMessageShouldNotLeakInternalDetails verifies that internal error details
|
|
// are not exposed to clients
|
|
func TestMED09_ErrorMessageShouldNotLeakInternalDetails(t *testing.T) {
|
|
secretKey := "test-secret-key-12345678901234567890"
|
|
issuer := "test-issuer"
|
|
|
|
// Create middleware with a token that will cause an error
|
|
middleware := &AuthMiddleware{
|
|
config: AuthConfig{
|
|
SecretKey: secretKey,
|
|
Issuer: issuer,
|
|
},
|
|
tokenCache: NewTokenCache(),
|
|
// Intentionally no tokenBackend - to simulate error scenario
|
|
}
|
|
|
|
nextHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
// Next handler should not be called for auth failures
|
|
})
|
|
|
|
handler := middleware.TokenVerifyMiddleware(nextHandler)
|
|
|
|
// Create a token that will fail verification
|
|
// Using wrong signing key to simulate internal error
|
|
claims := TokenClaims{
|
|
RegisteredClaims: jwt.RegisteredClaims{
|
|
Issuer: issuer,
|
|
Subject: "subject:1",
|
|
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)),
|
|
IssuedAt: jwt.NewNumericDate(time.Now()),
|
|
},
|
|
SubjectID: "subject:1",
|
|
Role: "owner",
|
|
Scope: []string{"read", "write"},
|
|
TenantID: 1,
|
|
}
|
|
|
|
// Sign with wrong key to cause error
|
|
wrongKeyToken := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
|
wrongKeyTokenString, _ := wrongKeyToken.SignedString([]byte("wrong-secret-key-that-will-cause-error"))
|
|
|
|
// Create request with Bearer token
|
|
req := httptest.NewRequest("POST", "/api/v1/test", nil)
|
|
ctx := context.WithValue(req.Context(), bearerTokenKey, wrongKeyTokenString)
|
|
req = req.WithContext(ctx)
|
|
|
|
w := httptest.NewRecorder()
|
|
handler.ServeHTTP(w, req)
|
|
|
|
// Should return 401
|
|
if w.Code != http.StatusUnauthorized {
|
|
t.Errorf("expected status 401, got %d", w.Code)
|
|
}
|
|
|
|
// Parse response
|
|
var resp map[string]interface{}
|
|
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
|
t.Fatalf("failed to parse response: %v", err)
|
|
}
|
|
|
|
// Check error map
|
|
errorMap, ok := resp["error"].(map[string]interface{})
|
|
if !ok {
|
|
t.Fatal("response should contain error object")
|
|
}
|
|
|
|
message, ok := errorMap["message"].(string)
|
|
if !ok {
|
|
t.Fatal("error should contain message")
|
|
}
|
|
|
|
// The error message should NOT contain internal details like:
|
|
// - "crypto" or "cipher" related terms (implementation details)
|
|
// - "secret", "key", "password" (credential info)
|
|
// - "SQL", "database", "connection" (database details)
|
|
// - File paths or line numbers
|
|
|
|
internalKeywords := []string{
|
|
"crypto/",
|
|
"/go/src/",
|
|
".go:",
|
|
"sql",
|
|
"database",
|
|
"connection",
|
|
"pq",
|
|
"pgx",
|
|
}
|
|
|
|
for _, keyword := range internalKeywords {
|
|
if strings.Contains(strings.ToLower(message), keyword) {
|
|
t.Errorf("MED-09: error message should NOT contain internal details like '%s'. Got: %s", keyword, message)
|
|
}
|
|
}
|
|
|
|
// The message should be a generic user-safe message
|
|
if message == "" {
|
|
t.Error("error message should not be empty")
|
|
}
|
|
}
|
|
|
|
// TestMED09_TokenVerifyErrorShouldBeSanitized tests that token verification errors
|
|
// don't leak sensitive information
|
|
func TestMED09_TokenVerifyErrorShouldBeSanitized(t *testing.T) {
|
|
secretKey := "test-secret-key-12345678901234567890"
|
|
issuer := "test-issuer"
|
|
|
|
// Create middleware
|
|
m := &AuthMiddleware{
|
|
config: AuthConfig{
|
|
SecretKey: secretKey,
|
|
Issuer: issuer,
|
|
},
|
|
}
|
|
|
|
// Test with various invalid tokens
|
|
invalidTokens := []struct {
|
|
name string
|
|
token string
|
|
expectError bool
|
|
}{
|
|
{
|
|
name: "completely invalid token",
|
|
token: "not.a.valid.token.at.all",
|
|
expectError: true,
|
|
},
|
|
{
|
|
name: "expired token",
|
|
token: createExpiredTestToken(secretKey, issuer),
|
|
expectError: true,
|
|
},
|
|
{
|
|
name: "wrong issuer token",
|
|
token: createWrongIssuerTestToken(secretKey, issuer),
|
|
expectError: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range invalidTokens {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
_, err := m.verifyToken(tt.token)
|
|
|
|
if tt.expectError && err == nil {
|
|
t.Error("expected error but got nil")
|
|
}
|
|
|
|
if err != nil {
|
|
errMsg := err.Error()
|
|
|
|
// Internal error messages should be sanitized
|
|
// They should NOT contain sensitive keywords
|
|
sensitiveKeywords := []string{
|
|
"secret",
|
|
"password",
|
|
"credential",
|
|
"/",
|
|
".go:",
|
|
}
|
|
|
|
for _, keyword := range sensitiveKeywords {
|
|
if strings.Contains(strings.ToLower(errMsg), keyword) {
|
|
t.Errorf("MED-09: internal error should NOT contain '%s'. Got: %s", keyword, errMsg)
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// Helper function to create expired token
|
|
func createExpiredTestToken(secretKey, issuer string) string {
|
|
claims := TokenClaims{
|
|
RegisteredClaims: jwt.RegisteredClaims{
|
|
Issuer: issuer,
|
|
Subject: "subject:1",
|
|
ExpiresAt: jwt.NewNumericDate(time.Now().Add(-time.Hour)), // Expired
|
|
IssuedAt: jwt.NewNumericDate(time.Now().Add(-2 * time.Hour)),
|
|
},
|
|
SubjectID: "subject:1",
|
|
Role: "owner",
|
|
Scope: []string{"read", "write"},
|
|
TenantID: 1,
|
|
}
|
|
|
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
|
tokenString, _ := token.SignedString([]byte(secretKey))
|
|
return tokenString
|
|
}
|
|
|
|
// Helper function to create wrong issuer token
|
|
func createWrongIssuerTestToken(secretKey, issuer string) string {
|
|
claims := TokenClaims{
|
|
RegisteredClaims: jwt.RegisteredClaims{
|
|
Issuer: "wrong-issuer",
|
|
Subject: "subject:1",
|
|
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)),
|
|
IssuedAt: jwt.NewNumericDate(time.Now()),
|
|
},
|
|
SubjectID: "subject:1",
|
|
Role: "owner",
|
|
Scope: []string{"read", "write"},
|
|
TenantID: 1,
|
|
}
|
|
|
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
|
tokenString, _ := token.SignedString([]byte(secretKey))
|
|
return tokenString
|
|
} |