diff --git a/backend/internal/handler/api_key_handler.go b/backend/internal/handler/api_key_handler.go index 3363230815..6361b97dd6 100644 --- a/backend/internal/handler/api_key_handler.go +++ b/backend/internal/handler/api_key_handler.go @@ -3,6 +3,8 @@ package handler import ( "context" + "errors" + "math" "strconv" "strings" "time" @@ -62,6 +64,43 @@ type UpdateAPIKeyRequest struct { ResetRateLimitUsage *bool `json:"reset_rate_limit_usage"` // 重置限速用量 } +func validAPIKeyLimit(v float64) bool { return !math.IsNaN(v) && !math.IsInf(v, 0) && v >= 0 } + +func validateAPIKeyCreateRequest(req CreateAPIKeyRequest) error { + if req.Quota != nil && !validAPIKeyLimit(*req.Quota) { + return errors.New("invalid quota") + } + if req.RateLimit5h != nil && !validAPIKeyLimit(*req.RateLimit5h) { + return errors.New("invalid rate_limit_5h") + } + if req.RateLimit1d != nil && !validAPIKeyLimit(*req.RateLimit1d) { + return errors.New("invalid rate_limit_1d") + } + if req.RateLimit7d != nil && !validAPIKeyLimit(*req.RateLimit7d) { + return errors.New("invalid rate_limit_7d") + } + if req.ExpiresInDays != nil && *req.ExpiresInDays <= 0 { + return errors.New("invalid expires_in_days") + } + return nil +} + +func validateAPIKeyUpdateRequest(req UpdateAPIKeyRequest) error { + if req.Quota != nil && !validAPIKeyLimit(*req.Quota) { + return errors.New("invalid quota") + } + if req.RateLimit5h != nil && !validAPIKeyLimit(*req.RateLimit5h) { + return errors.New("invalid rate_limit_5h") + } + if req.RateLimit1d != nil && !validAPIKeyLimit(*req.RateLimit1d) { + return errors.New("invalid rate_limit_1d") + } + if req.RateLimit7d != nil && !validAPIKeyLimit(*req.RateLimit7d) { + return errors.New("invalid rate_limit_7d") + } + return nil +} + // List handles listing user's API keys with pagination // GET /api/v1/api-keys func (h *APIKeyHandler) List(c *gin.Context) { @@ -152,6 +191,10 @@ func (h *APIKeyHandler) Create(c *gin.Context) { response.BadRequest(c, "Invalid request: "+err.Error()) return } + if err := validateAPIKeyCreateRequest(req); err != nil { + response.BadRequest(c, "Invalid request: numeric limits must be finite and non-negative, and expires_in_days must be greater than zero") + return + } svcReq := service.CreateAPIKeyRequest{ Name: req.Name, @@ -203,6 +246,10 @@ func (h *APIKeyHandler) Update(c *gin.Context) { response.BadRequest(c, "Invalid request: "+err.Error()) return } + if err := validateAPIKeyUpdateRequest(req); err != nil { + response.BadRequest(c, "Invalid request: numeric limits must be finite and non-negative") + return + } svcReq := service.UpdateAPIKeyRequest{ IPWhitelist: req.IPWhitelist, diff --git a/backend/internal/handler/api_key_handler_validation_test.go b/backend/internal/handler/api_key_handler_validation_test.go new file mode 100644 index 0000000000..9a55e3535f --- /dev/null +++ b/backend/internal/handler/api_key_handler_validation_test.go @@ -0,0 +1,43 @@ +//go:build unit + +package handler + +import ( + "math" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestValidateAPIKeyCreateRequest(t *testing.T) { + zero, large, negative, nan, inf := 0.0, 1e100, -1.0, math.NaN(), math.Inf(1) + positiveDays, zeroDays, negativeDays := 1, 0, -1 + require.NoError(t, validateAPIKeyCreateRequest(CreateAPIKeyRequest{})) + require.NoError(t, validateAPIKeyCreateRequest(CreateAPIKeyRequest{Quota: &zero, RateLimit5h: &large, ExpiresInDays: &positiveDays})) + + for _, req := range []CreateAPIKeyRequest{ + {Quota: &negative}, + {Quota: &nan}, + {RateLimit5h: &inf}, + {RateLimit1d: &negative}, + {RateLimit7d: &negative}, + {ExpiresInDays: &zeroDays}, + {ExpiresInDays: &negativeDays}, + } { + require.Error(t, validateAPIKeyCreateRequest(req)) + } +} + +func TestValidateAPIKeyUpdateRequest(t *testing.T) { + zero, large, negative, nan, inf := 0.0, 1e100, -1.0, math.NaN(), math.Inf(-1) + require.NoError(t, validateAPIKeyUpdateRequest(UpdateAPIKeyRequest{Quota: &zero, RateLimit7d: &large})) + + for _, req := range []UpdateAPIKeyRequest{ + {Quota: &negative}, + {RateLimit5h: &nan}, + {RateLimit1d: &inf}, + {RateLimit7d: &negative}, + } { + require.Error(t, validateAPIKeyUpdateRequest(req)) + } +} diff --git a/backend/internal/service/api_key_service.go b/backend/internal/service/api_key_service.go index 2197912c9a..8ce62a688f 100644 --- a/backend/internal/service/api_key_service.go +++ b/backend/internal/service/api_key_service.go @@ -6,6 +6,7 @@ import ( "encoding/hex" "fmt" "html" + "math" "sort" "strconv" "strings" @@ -245,6 +246,36 @@ type UpdateAPIKeyRequest struct { ResetRateLimitUsage *bool `json:"reset_rate_limit_usage"` // Reset all usage counters to 0 } +func validateAPIKeyLimit(v float64) error { + if math.IsNaN(v) || math.IsInf(v, 0) || v < 0 { + return infraerrors.BadRequest("API_KEY_LIMIT_INVALID", "API key limits must be finite and non-negative") + } + return nil +} + +func validateCreateAPIKeyRequest(req CreateAPIKeyRequest) error { + for _, v := range []float64{req.Quota, req.RateLimit5h, req.RateLimit1d, req.RateLimit7d} { + if err := validateAPIKeyLimit(v); err != nil { + return err + } + } + if req.ExpiresInDays != nil && *req.ExpiresInDays <= 0 { + return infraerrors.BadRequest("API_KEY_EXPIRY_INVALID", "expires_in_days must be greater than zero") + } + return nil +} + +func validateUpdateAPIKeyRequest(req UpdateAPIKeyRequest) error { + for _, v := range []*float64{req.Quota, req.RateLimit5h, req.RateLimit1d, req.RateLimit7d} { + if v != nil { + if err := validateAPIKeyLimit(*v); err != nil { + return err + } + } + } + return nil +} + // APIKeyService API Key服务 // RateLimitCacheInvalidator invalidates rate limit cache entries on manual reset. type RateLimitCacheInvalidator interface { @@ -428,6 +459,9 @@ func (s *APIKeyService) canUserBindGroup(ctx context.Context, user *User, group // Create 创建API Key func (s *APIKeyService) Create(ctx context.Context, userID int64, req CreateAPIKeyRequest) (*APIKey, error) { + if err := validateCreateAPIKeyRequest(req); err != nil { + return nil, err + } // 验证用户存在 user, err := s.userRepo.GetByID(ctx, userID) if err != nil { @@ -723,6 +757,9 @@ func (s *APIKeyService) GetByKey(ctx context.Context, key string) (*APIKey, erro // Update 更新API Key func (s *APIKeyService) Update(ctx context.Context, id int64, userID int64, req UpdateAPIKeyRequest) (*APIKey, error) { + if err := validateUpdateAPIKeyRequest(req); err != nil { + return nil, err + } apiKey, err := s.apiKeyRepo.GetByID(ctx, id) if err != nil { return nil, fmt.Errorf("get api key: %w", err) diff --git a/backend/internal/service/api_key_service_validation_test.go b/backend/internal/service/api_key_service_validation_test.go new file mode 100644 index 0000000000..fc15c0b9a6 --- /dev/null +++ b/backend/internal/service/api_key_service_validation_test.go @@ -0,0 +1,46 @@ +//go:build unit + +package service + +import ( + "math" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestValidateCreateAPIKeyRequestNumericLimits(t *testing.T) { + positiveExpiry := 1 + require.NoError(t, validateCreateAPIKeyRequest(CreateAPIKeyRequest{ + Quota: 1e100, RateLimit5h: 1e100, ExpiresInDays: &positiveExpiry, + })) + require.NoError(t, validateCreateAPIKeyRequest(CreateAPIKeyRequest{})) + + invalidExpiry := 0 + tests := []CreateAPIKeyRequest{ + {Quota: -1}, + {Quota: math.NaN()}, + {Quota: math.Inf(1)}, + {RateLimit5h: -1}, + {RateLimit1d: math.NaN()}, + {RateLimit7d: math.Inf(-1)}, + {ExpiresInDays: &invalidExpiry}, + } + for _, req := range tests { + require.Error(t, validateCreateAPIKeyRequest(req)) + } +} + +func TestValidateUpdateAPIKeyRequestNumericLimits(t *testing.T) { + zero, large, negative, nan, inf := 0.0, 1e100, -1.0, math.NaN(), math.Inf(1) + require.NoError(t, validateUpdateAPIKeyRequest(UpdateAPIKeyRequest{Quota: &zero, RateLimit7d: &large})) + + for _, req := range []UpdateAPIKeyRequest{ + {Quota: &negative}, + {RateLimit5h: &nan}, + {RateLimit1d: &inf}, + {RateLimit7d: &negative}, + } { + require.Error(t, validateUpdateAPIKeyRequest(req)) + } +}