← 返回首页
逆向2026 · 07 · 2845 分钟阅读

0 PHP 白嫖 ChatGPT Plus — 跨区定价混淆攻击的完整技术拆解

前言

这两天群里炸了。

有人甩出一张截图:Stripe 结账页面,产品写着 “ChatGPT Plus”,金额一栏赫然显示 ₱0.00 PHP。菲律宾比索,零元,提交即开通。

评论区画风可想而知——“假的吧"“PS 的吧"“你怎么不说 Plus 倒找你钱”。

然后陆陆续续有人贴出了完整流程,甚至还配了视频录屏。操作步骤不复杂,总共四步,不需要 Root,不需要 Frida,不需要改一行代码。唯一的技术含量在于——你得知道用什么顺序把哪些区域串起来。

这就有意思了。之前我们拆过 prorationMode hook(改一个 putInt 白嫖 Claude Max)、拆过 焚决脚本(劫持 checkout_capabilities 走 SEPA 假账号)、拆过 RevenueCat 凭证转移(匿名购买 + restore 归属偷渡)。那些多少都是"改代码"的路子——Hook 内存、注入响应、篡改参数。

但这次不一样。这次什么都没改。没有脚本,没有 Hook,没有中间人。攻击者只是以一种特定的顺序访问了几个网页,然后 Stripe 自己就吐出了 0 元账单。

今天把它完整拆开。

一、攻击全景:四步,从 JP 到 PH,从 $20 到 ₱0

先把流传的步骤还原一下:

Step 1:日本节点注册 OpenAI 账号

挂 JP 代理,开一个干净的浏览器 Profile(指纹浏览器或者 Chrome 新 Profile),去 OpenAI 官网正常注册,邮箱验证,登录确认。

此时你的账号状态:

account.locale = "ja-JP"
account.billing_country = "JP"
account.currency = "JPY"

OpenAI 在注册时根据你的 IP 地理位置初始化账号的计费区域。这一步的目的不是"要日本"本身,而是要一个非美区的初始定价锚点。

Step 2:切美国节点,提取 Access Token

保持同一个浏览器 Profile,VPN 切到 US 节点,重新登录刚才注册的账号。然后新标签页打开:

https://chatgpt.com/api/auth/session

浏览器返回一个 JSON,里面有 accessToken 字段——一个标准 JWT。复制保存。

此时你的 session 状态出现了第一层矛盾:

account.billing_country = "JP"  ← 注册时写入,持久化在 OpenAI 数据库
session.ip_country = "US"       ← 当前 IP 推断,存在 session/edge 层

Step 3:第三方工具生成 Checkout 链接

打开某个第三方"CDK 提炼"工具站点(这类工具本质上就是一个 AT → Stripe Checkout Session 的代理),把 Access Token 粘进去,点生成。

工具返回一个链接,格式:

https://chatgpt.com/checkout/openai_llc/oaics_xxxxxxxxxxxxxxxxxxxxx

oaics_ 是 OpenAI 的 Stripe Checkout Session ID 前缀。这个 session 是 OpenAI 后端基于你的 AT 创建的,包含了产品、价格、货币等信息。

关键问题来了:这个 Checkout Session 的 currency 和 amount 是怎么决定的?

Step 4:打开链接,惊喜时刻

在美国节点下打开这个 oaics 链接。Stripe 结账页面加载出来,你看到:

ChatGPT Plus
₱0.00 PHP / month

菲律宾比索。零元。

填入指定 BIN 的卡片信息(523686 或 4513),美国账单地址,提交。

Stripe 收了一笔 0 元交易,OpenAI 后端确认 checkout session 成功,权益生效。回到 ChatGPT 首页,左上角已经是 Plus 了。

二、为什么是 0 PHP?——三种假说

先澄清一个重要事实:OpenAI 在菲律宾是有明确定价的,ChatGPT Plus 的 PH 区定价大约是 ₱990 PHP/月。所以这不是简单的"价格表空洞导致 fallback 到 0”——如果定价系统正常查 PH 价格表,应该返回 ₱990,不是 ₱0。

那 0 是怎么来的?没有对 Checkout Session 创建过程的完整抓包,我们只能做推断。以下三种假说按可能性排序。

假说一:第三方工具在创建 Checkout Session 时注入了零元参数(最可能)

OpenAI 的 /backend-api/payments/checkout 接口接受多个参数来创建 Stripe Checkout Session。如果这个接口接受客户端传入的 discount、coupon、promotion_code 或类似参数,且后端没做资格校验——工具直接传了一个 100% 折扣的 coupon code 或者 amount_override: 0。

这和之前 RevenueCat 文章里分析过的 eligible_promo_campaigns 注入如出一辙:客户端声称"我有优惠资格”,后端不校验就信了。

<span class="line"><span class="cl"><span class="c1">// 工具的真实请求可能长这样
</span></span></span><span class="line"><span class="cl"><span class="nx">body</span><span class="o">:</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">  <span class="nx">plan</span><span class="o">:</span> <span class="s2">"plus"</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nx">promotion_code</span><span class="o">:</span> <span class="s2">"某个内部免费试用码"</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="c1">// 或者
</span></span></span><span class="line"><span class="cl">  <span class="nx">billing_country</span><span class="o">:</span> <span class="s2">"PH"</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nx">currency</span><span class="o">:</span> <span class="s2">"php"</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="c1">// 或者更直接的
</span></span></span><span class="line"><span class="cl">  <span class="nx">price_override</span><span class="o">:</span> <span class="mi">0</span>
</span></span><span class="line"><span class="cl"><span class="p">})</span>
</span></span>

JP 注册 + US 登录制造的区域歧义,可能不是直接触发 0 元的原因,而是绕过风控检查的前置条件——当账号区域和 IP 区域不一致时,后端对 checkout 参数的校验逻辑可能走入了一个宽松的分支。

假说二:跨区信号冲突导致定价路由进入异常分支

当 OpenAI 创建 Stripe Checkout Session 时,面前有至少四个区域信号:

| 信号来源 | 值 | 决定时机 | | 账号注册地 | JP | 注册时固化 | | 当前 session IP | US | 每次请求实时推断 | | Stripe 卡 BIN 发行国 | PH | 填卡时由 Stripe 推断 | | 浏览器 Accept-Language | 取决于浏览器设置 | 每次请求携带 |

正常场景下这四个信号一致——美国人在美国用美国卡买东西,直接查 US 价格表。

但当 JP ≠ US ≠ PH 三个信号同时存在时,定价系统需要做一个选择。如果选择逻辑是 if (account.country != ip.country) { use card.country } 这种优先级策略,而 PH 价格表虽然存在但在某个子路径(比如"非本地注册用户+跨区IP"的特殊定价分支)中没有配置——就可能在这个特殊分支里 fallback 到 0。

换句话说:PH 的主价格表有值(₱990),但某个异常处理路径的价格映射没覆盖到 PH,攻击者通过区域信号碎片化精确命中了这条路径。

假说三:Stripe Checkout Session 的 line_items 被工具篡改

Stripe checkout.sessions.create 的 line_items 参数包含 price 或 price_data。如果 OpenAI 的接口允许客户端指定 price_data.unit_amount(哪怕是间接的),工具就能在创建 session 时直接把金额写成 0。

line_items: [{
  price_data: {
    currency: "php",
    unit_amount: 0,        // 直接指定 0
    product: "prod_xxx",
    recurring: { interval: "month" }
  },
  quantity: 1
}]

这种情况下 PH BIN 的作用不是"触发 PH 价格表",而是让 currency 字段说得通——如果你传 currency: "php" 但卡是 US 卡,Stripe 可能会有额外的 currency mismatch 警告。PH 卡 + PHP 货币是自洽的。

为什么是 PH BIN?

无论哪种假说,流传的两个 BIN 都有其作用:

| BIN | 网络 | 发行国 | | 523686 | Mastercard | 菲律宾 | | 4513xx | Visa | 取决于具体发行行 |

523686 开头的卡填入 Stripe 表单时,Stripe 内部标记 payment_method.card.country = "PH"。这个信号在整个支付流中传播。PH BIN 的作用至少有两个:

  • 让 PHP 货币的 checkout session 能正常完成——卡的发行国和结账货币匹配
  • 可能绕过 OpenAI 的 card-country vs account-country 风控规则——PH 卡付 PHP 金额在 Stripe 层面是"正常交易"

三、JP 注册的作用——制造"区域身份分裂"

你可能会问:为什么不直接在美国注册、美国提 Token、用 PH 卡付?

答案是:直接美区注册的账号,定价系统的行为是确定性的——走 US 价格表,$20 USD,没有歧义可利用。

JP 注册的作用是制造一个区域身份不确定的账号。当账号的 billing_country 是 JP,session IP 是 US,这两个信号已经矛盾了。定价系统在处理这种矛盾时,可能会:

  • 走入一个异常处理分支,该分支的参数校验比正常路径更宽松
  • 允许第三方工具注入的额外参数(如 currency、discount)覆盖默认行为
  • 使用一个与主价格表不同的备用定价逻辑,而这个逻辑有漏洞

用状态机的视角看:

                  ┌──────────────┐
                  │ 注册时: JP    │
                  │ locale=ja-JP │
                  └──────┬───────┘
                         │
                  ┌──────▼───────┐
                  │ 登录时: US    │
                  │ ip_country=US│
                  │ ≠ locale     │
                  └──────┬───────┘
                         │
                  ┌──────▼───────────┐
                  │ 创建 Checkout    │
                  │ JP ≠ US → 歧义   │
                  │ → 进入异常分支   │
                  │ → 参数校验宽松   │
                  └──────┬───────────┘
                         │
                  ┌──────▼───────────┐
                  │ 工具注入参数     │
                  │ + Card BIN: PH   │
                  │ → amount = 0 PHP │
                  └──────────────────┘

三个不同的区域信号(JP / US / PH)制造了歧义,歧义打开了异常路径,异常路径上的校验缺失被利用。

这是一个经典的安全反模式:当多个权威源对同一个问题给出不同答案时,系统在歧义中放松了校验——不是直接返回 0,而是给了攻击者操纵结果的空间。

四、第三方"提炼工具"在干什么?

流程里的那个第三方网站(sms.linlinflow.ccwu.cc),名字叫"CDK 提炼",听着玄乎,其实它干的事非常直白:

  • 接收你的 Access Token
  • 用这个 AT 调用 OpenAI 后端 API,创建一个 Stripe Checkout Session
  • 把 Checkout Session 的 URL 返回给你

本质就是一个 AT → oaics_ 链接 的代理服务。

它的技术实现大概率是这样的:

<span class="line"><span class="cl"><span class="c1">// 伪代码,非真实接口
</span></span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">response</span> <span class="o">=</span> <span class="kr">await</span> <span class="nx">fetch</span><span class="p">(</span><span class="s2">"https://chatgpt.com/backend-api/payment/checkout"</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nx">method</span><span class="o">:</span> <span class="s2">"POST"</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nx">headers</span><span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="s2">"Authorization"</span><span class="o">:</span> <span class="sb">`Bearer </span><span class="si">${</span><span class="nx">accessToken</span><span class="si">}</span><span class="sb">`</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">"Content-Type"</span><span class="o">:</span> <span class="s2">"application/json"</span>
</span></span><span class="line"><span class="cl">  <span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="nx">body</span><span class="o">:</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">    <span class="nx">plan</span><span class="o">:</span> <span class="s2">"plus"</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="c1">// 关键:这里可能注入了额外参数
</span></span></span><span class="line"><span class="cl">    <span class="c1">// billing_country? currency? locale?
</span></span></span><span class="line"><span class="cl">  <span class="p">})</span>
</span></span><span class="line"><span class="cl"><span class="p">});</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="p">{</span> <span class="nx">checkout_url</span> <span class="p">}</span> <span class="o">=</span> <span class="kr">await</span> <span class="nx">response</span><span class="p">.</span><span class="nx">json</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"><span class="c1">// checkout_url = "https://chatgpt.com/checkout/openai_llc/oaics_..."
</span></span></span>

关键问题:工具只是"转发"了 AT,还是"动了手脚"?

考虑到 PH 区有明确的 Plus 定价(约 ₱990/月),而结果是 ₱0——工具大概率不只是简单转发。它在创建 Checkout Session 时注入了额外参数,利用跨区账号的异常处理路径绕过了服务端校验。

具体注入了什么参数?没有抓包数据我们无法确定。可能是 promotion_code、discount、currency + amount 覆盖、甚至是一个 OpenAI 内部的免费试用 API 路径。但有一点可以确定:如果工具只是忠实转发 AT,以 PH 区 ₱990 的定价,结果不可能是 ₱0。

工具是这个攻击链中最不透明的环节。你把 Access Token——等同于你的登录态——交给了一个第三方服务。它用你的身份做了什么请求、传了什么参数,你完全不知道。这也是为什么整个流程的"技术含量"看似很低——核心逻辑都藏在工具服务端里,用户只是按步骤操作。

五、BIN 选择——不是随便什么卡都行

流传的步骤里特别强调了两个 BIN:523686(Mastercard)和 4513(Visa)。这不是随便挑的。

BIN 在支付流中的角色

BIN(Bank Identification Number)是卡号的前 6-8 位,编码了发卡行、卡网络、卡类型和发行国等信息。Stripe 在收到卡号的前 6 位时就能确定:

523686 → Mastercard / Philippines / Credit / 某发卡行

这个信息会被写入 payment_method.card.country,并在整个支付流中传播。

为什么必须是 PH BIN?

PH BIN 的作用不是"触发一个空的价格表"——PH 价格表是存在的(约 ₱990/月)。

更可能的解释是:

  • 货币匹配:如果工具创建的 Checkout Session 指定了 currency: "php",那么 PH 发行的卡才能让 Stripe 的 currency-card 一致性检查通过。US 卡 + PHP 货币会触发额外的风控审查。
  • 风控绕过:OpenAI/Stripe 的风控系统可能对"card country = PH + checkout currency = PHP"这个组合判定为"正常的本地交易",不会触发跨境支付的额外校验。而如果用 JP 卡或 US 卡来付一笔 0 PHP 的订单,风控更容易拦截。
  • AVS 绕过:PH 卡不走 Stripe 的 AVS 地址校验,所以填什么地址都行——这降低了被拒的概率。

523686 和 4513 是经过实测确认能被 Stripe 正确识别为 card.country = "PH" 的 BIN 段。不是所有 PH BIN 都行,因为某些 BIN 可能已被 OpenAI/Stripe 的风控规则标记或黑名单。

为什么要填美国账单地址?

这步看起来矛盾——卡是菲律宾的,地址填美国?

原因是 Stripe 的 AVS(Address Verification Service)只对美国和英国的卡做地址校验。PH 卡不走 AVS,所以你填什么地址都无所谓——但如果你填 PH 地址,可能触发 OpenAI 的额外风控逻辑。填 US 地址是为了"看起来正常",降低被风控拦截的概率。

六、从防御视角看——OpenAI 需要修什么

不管具体是哪种假说成立,防御原则是通用的。

修复方案(从简单到全面)

Level 1:Checkout Session 金额校验

<span class="line"><span class="cl"><span class="k">def</span> <span class="nf">create_checkout_session</span><span class="p">(</span><span class="n">user</span><span class="p">,</span> <span class="n">plan</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">price</span> <span class="o">=</span> <span class="n">get_price</span><span class="p">(</span><span class="n">plan</span><span class="p">,</span> <span class="n">resolve_country</span><span class="p">(</span><span class="n">user</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">price</span><span class="p">[</span><span class="s2">"amount"</span><span class="p">]</span> <span class="o">==</span> <span class="mi">0</span> <span class="ow">and</span> <span class="n">plan</span> <span class="o">!=</span> <span class="s2">"free"</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">raise</span> <span class="ne">ValueError</span><span class="p">(</span><span class="s2">"Zero-amount checkout for paid plan"</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># ...</span>
</span></span>

付费产品金额为 0 时直接拒绝创建 session。这应该是支付系统的基本不变量。

Level 3:区域一致性校验

<span class="line"><span class="cl"><span class="k">def</span> <span class="nf">resolve_country</span><span class="p">(</span><span class="n">user</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">signals</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="s2">"registration"</span><span class="p">:</span> <span class="n">user</span><span class="o">.</span><span class="n">billing_country</span><span class="p">,</span>    <span class="c1"># JP</span>
</span></span><span class="line"><span class="cl">        <span class="s2">"session_ip"</span><span class="p">:</span> <span class="n">geoip</span><span class="p">(</span><span class="n">user</span><span class="o">.</span><span class="n">current_ip</span><span class="p">),</span>     <span class="c1"># US</span>
</span></span><span class="line"><span class="cl">        <span class="s2">"card_bin"</span><span class="p">:</span> <span class="kc">None</span><span class="p">,</span>  <span class="c1"># 创建 session 时还不知道</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># 如果注册地和 session IP 不一致,以注册地为准</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># 不让 card BIN 覆盖定价区域</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">signals</span><span class="p">[</span><span class="s2">"registration"</span><span class="p">]</span>
</span></span>

定价的 country 由一个确定性函数决定,不受 card BIN 影响。Card BIN 的 country 只用于风控信号,不参与定价。

Level 4:Webhook 后置校验

<span class="line"><span class="cl"><span class="k">def</span> <span class="nf">on_checkout_completed</span><span class="p">(</span><span class="n">event</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">session</span> <span class="o">=</span> <span class="n">event</span><span class="o">.</span><span class="n">data</span><span class="o">.</span><span class="n">object</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">session</span><span class="o">.</span><span class="n">amount_total</span> <span class="o">==</span> <span class="mi">0</span> <span class="ow">and</span> <span class="n">session</span><span class="o">.</span><span class="n">mode</span> <span class="o">==</span> <span class="s2">"subscription"</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># 0 元订阅?不对劲</span>
</span></span><span class="line"><span class="cl">        <span class="n">stripe</span><span class="o">.</span><span class="n">Subscription</span><span class="o">.</span><span class="n">delete</span><span class="p">(</span><span class="n">session</span><span class="o">.</span><span class="n">subscription</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">alert</span><span class="p">(</span><span class="s2">"Zero-amount subscription detected"</span><span class="p">,</span> <span class="n">session</span><span class="p">)</span>
</span></span>

即使 Checkout Session 创建时金额为 0,在 checkout.session.completed webhook 里也要做最终校验。这是最后一道防线。

七、这个漏洞和之前的几个有什么关系?

放在一起看,这几个月被扒出来的 ChatGPT/Claude 支付漏洞其实形成了一个谱系:

| 漏洞 | 攻击层 | 改了什么 | 核心缺陷 | | prorationMode Hook | 客户端内存 | putInt 的一个参数 | Google Play 信任客户端传入的升级策略 | | 焚决 (cassia) | HTTP 响应 | checkout_flow 字段 | 前端根据 API 响应选择支付通道,后端不校验 | | RevenueCat 转移 | 聚合器 API | is_restore + app_user_id | 匿名购买 + restore 归属无限转移 | | API 响应注入 | HTTP 响应 | eligible_promo_campaigns | 优惠码资格校验仅在前端 | | 0 PHP 跨区 (本文) | 定价系统 | 区域信号 + 工具注入 | 跨区歧义打开异常路径 + checkout 参数校验缺失 |

注意到了吗?攻击层在不断"上移"。

早期的漏洞需要 Root、需要 Frida、需要改代码——门槛不低。焚决降到了一个油猴脚本的水平。而 0 PHP 攻击连脚本都不需要——纯操作流程,任何人都能复现。

从攻击者的角度看,这是一个自然演进:当客户端层的防御越来越强(代码混淆、完整性校验、证书绑定),攻击者就会转向更高层——业务逻辑层、定价系统层、区域路由层。 这些层的防御往往更薄弱,因为它们不像"加密"“签名"那样有明确的安全框架,它们是业务系统的一部分,安全评审时容易被忽略。

八、更通用的攻击模式——“区域信号碎片化”

0 PHP 攻击其实揭示了一个通用的攻击模式,我把它叫做“区域信号碎片化攻击”(Region Signal Fragmentation)

攻击模型

victim_system.pricing = f(country)

country = resolve(
  signal_1: registration_country,  ← 攻击者控制(注册时选择 VPN 节点)
  signal_2: session_ip_country,    ← 攻击者控制(登录时选择 VPN 节点)
  signal_3: card_bin_country,      ← 攻击者控制(选择特定 BIN 的卡)
  signal_4: browser_locale,        ← 攻击者控制(浏览器设置)
  signal_5: billing_address,       ← 攻击者控制(表单填写)
)

所有五个信号都由攻击者完全控制。如果 resolve() 函数对歧义输入的处理不当——比如进入异常分支后放松了参数校验,或者 fallback 逻辑允许客户端覆盖定价——攻击者就能把定价推入非预期状态。

防御原则

原则一:定价权归一

pricing_country = user.billing_country  // 只有一个来源,注册时确定
// card BIN、IP、locale 都不参与定价决策
// 只作为风控辅助信号

原则二:Checkout 参数不可客户端覆盖

# 服务端决定 price、currency、amount
# 客户端/第三方传入的 discount、coupon、price_override 一律忽略
# 只接受服务端白名单内的 promotion_code,且校验账号资格
checkout_session = stripe.checkout.Session.create(
    line_items=[server_determined_price],  # 不接受客户端 price_data
    discounts=[],                          # 不接受客户端 coupon
)

客户端能影响定价 = 攻击面。

原则三:零元红线

if checkout.amount == 0 and plan.type == "paid":
    REJECT()  // 无条件拒绝

付费产品不可能合法地出现 0 元。这应该是一个硬编码的不变量,不依赖任何业务逻辑。

九、写在最后

从 prorationMode 到焚决到 RevenueCat 到 0 PHP,每一个漏洞都指向同一个结构性问题:

当支付系统由多个独立组件(商店、聚合器、定价引擎、checkout 前端、webhook 后端)拼装而成时,组件之间的信任传递就是攻击面。

prorationMode 的信任传递是"Google Play 信任客户端传入的升级策略”。焚决的信任传递是"前端信任 API 返回的支付通道"。RevenueCat 的信任传递是"聚合器信任 SDK 传入的 app_user_id"。0 PHP 的信任传递是"定价系统信任 card BIN 推断的国家"。

没有一个单点漏洞,都是链路上的信任滑坡。

而防御的核心思路也始终一样:在链路的最末端(服务端 webhook、权益发放点)做最终校验,不信任链路上游传递的任何"结论",只信任可验证的"事实"。

具体到 0 PHP 这个 case:Stripe checkout.session.completed 事件里的 amount_total 是事实,currency 是事实。如果一个 ChatGPT Plus 订阅的 amount_total 是 0——不管前面经历了多少层区域路由和定价查询——这就不对。拦住它。

这比修价格表更重要。因为价格表总有遗漏,但"付费产品不能 0 元"这条规则没有例外。

往期相关:

  • Google Play 内购 CC Max 漏洞深度拆解
  • 焚决 Claude — 油猴脚本撬开支付大门
  • GPT Plus 订阅漏洞 — RevenueCat 凭证转移
  • GPT Plus 订阅漏洞 — Google Play Billing 鉴权缺失
原文出处blog.caowo.de