Skip to content

fix(billing): reject out-of-range image n before it multiplies the bill - #1211

Open
think-back wants to merge 1 commit into
mainfrom
fix/image-n-validation
Open

think-back wants to merge 1 commit into
mainfrom
fix/image-n-validation

Conversation

@think-back

Copy link
Copy Markdown
Collaborator

问题

2026-09-13 15:32 UTC,一条 gpt-image-2 请求被计费 $37,795,712(quota 18,897,856,098,000)。

user 22477 (google_1233900)  token=unlimited-key  channel=83(Codex)  HTTP 200  16s
"大小 256x256, 品质 standard, 生成数量 4294967295"

算式分毫不差:0.011 × 500000 × 0.8 × 4294967295 = 18,897,856,098,000

客户端传的 n 未经任何值域校验就进入计费乘法链(ImageHelperPriceData.OtherRatioscalculateTextQuotaSummary),其数值直接放大账单。

n 的来源

4294967295 = 2³²−1(uint32 上限)。实测标准库 encoding/json*uint 的行为,排除了所有「服务端算错」的可能:

客户端传 解析结果
-1 / -1.0 / "4294967295" / 4294967295.0 / 1e10 全部报错
4294967295 通过

Go 不做负数回绕、不接受字符串或浮点,唯一可能是请求体里原样写着该整数。推测为客户端把 -1 塞进 uint32 变量后回绕所致。

注:上游只生成了一张图(16s / 515 completion tokens),只收了一张图的成本。这 3779 万是纯内部记账放大,用户未多获取任何资源 —— 是自伤,不是套利。

双向缺口

  • 值过大n 无上限,直接作为乘数放大账单。
  • 值为零PriceData.AddOtherRatio 丢弃任何 ratio <= 0n=0 的倍率永不落地,请求按 n=1 计费;而该零值仍会原样透传给上游,各家 provider 解释不一。

relay/image_billing.go 已专门把 TotalTokens=0 抬为 1 来堵「零值导致不收费」,说明这类陷阱此前已被意识到,但漏掉了 n 这个维度。视频路径已有 zero/NaN/Inf 倍率防护测试,图片路径没有。

修改

校验 n ∈ [1, 10](OpenAI images API 官方上限,也不低于本网关对接的任一上游),越界在转发前返回 400。

校验点放在 ImageHelper 入口,紧邻同风格的 temp_url 400 检查。图片流量单一入口(controller/relay.go:40ImageHelper),覆盖全部图片路径。

验证

  • TDD:先写测试见红(undefined: validateImageN),再实现转绿
  • 4 组用例:uint32-max 复现值、n=0、上限+1、边界值(nil / 1 / 10)
  • 回归 go test ./relay/:baseline 与改动后均 ok,零新增失败
  • 改动纯新增 84 行、零删除;gofmt 干净

范围说明

本 PR 只关闭有实据的这一个字段。更广的缺口 —— 请求字段未经值域校验即参与价格乘法 —— 建议单独一轮排查。

另有两项不在本 PR 内、需业务决策:

  1. 计费熔断层:单请求 quota 超阈值即拒绝并告警。阈值属业务决策(正常 claude-opus-5 大请求可达 $30/次),未擅自设定。这层防的是下一个未知乘数字段,比单修 n 更根本。
  2. 余额可被扣穿为负DecreaseUserQuotamutateUserWalletQuotarequireAtLeast=0,而下限检查为 if RequireAtLeast > 0model/quota_lifecycle.go:109),条件恒不成立,故无任何下限保护。是有意设计还是疏漏,需确认。

取证补充

30 天日志保留期内逐段扫描,4294967295 仅出现这一次,非反复试探。
(排查提示:gcloud logging read --freshness 在该数据量下会返回假空,需改用精确 timestamp 时间窗分段查询。)

A client-supplied `n` flows unchecked into the quota multiplier chain
(ImageHelper -> PriceData.OtherRatios -> calculateTextQuotaSummary), so
its value scales the request price directly.

On 2026-09-13 a gpt-image-2 request arrived with n=4294967295 (uint32
max) and was billed 18,897,856,098,000 quota -- $37,795,712 for a single
request whose upstream returned one image in 16s. The arithmetic checks
out exactly: 0.011 * 500000 * 0.8 * 4294967295.

n=0 is the same gap from the other side: AddOtherRatio drops any ratio
<= 0, so the multiplier never lands and the request bills as if n=1
while the zero is still forwarded upstream.

Validate n in [1, 10] (OpenAI's own ceiling, and at or above every
upstream this gateway fronts) and reject anything else with a 400 before
the request is relayed. Image traffic has a single entry point
(controller/relay.go -> ImageHelper), so this covers every image path.

This closes the one field we have evidence of. The broader gap -- request
fields reaching the price multiplier without a value-domain check -- is
worth a separate pass; the video path already guards zero/NaN/Inf ratios
while the image path did not.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant