Merge pull request #5464 from wucm667/fix/issue-5455-api-key-input-validation

fix(api-key): validate quota and expiry inputs
This commit is contained in:
Wesley Liddick
2026-08-10 10:52:04 +08:00
committed by GitHub
4 changed files with 173 additions and 0 deletions
@@ -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,
@@ -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))
}
}
@@ -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)
@@ -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))
}
}