支付逻辑漏洞八大姿势(上):金额篡改、优惠券滥用、回调篡改与越权订单
2026/9/15 16:51:18 网站建设 项目流程

在接收支付相关的安全测试任务时,我最常说的一句话是:搞业务逻辑漏洞,拼的不是手里有多少高深的扫描器,而是能不能顺着业务流转的链路,把每个环节的“信任点”逐一挑出来盘问一遍。信任点越隐蔽,往往越危险。支付这类强业务、强交互、强资金属性的功能,天然就是逻辑漏洞的重灾区——开发在赶版本时最容易顺手写下“客户端传入什么,后端就信什么”的代码。这类问题埋得深、触发面广,自动化工具经常扫不出来,但一旦被人盯上,轻则刷单薅羊毛,重则资金被批量转空。

中途接手过一套电商系统的安全复审之后我才意识到,支付链路的坑远不止“改个价格”那么简单。网上关于支付逻辑漏洞的盘点不少,但大多是零散案例堆砌。我把这些年见到的、实测过的、以及同行分享过的典型问题归纳成八个大类,准备分上下两篇来讲。上篇先聊前四类:金额篡改、优惠券与积分滥用、支付回调篡改、越权订单操作。这四类共同点是“攻击者手里能改的参数特别多”,非常适合理清支付链路的基本信任边界。

1. 支付逻辑漏洞全景与拆解思路

1.1 什么是支付逻辑漏洞,它为什么频繁出现

通俗点讲,支付逻辑漏洞就是“业务规则没有被正确执行”。用户本应该按照价格A、数量B、优惠C去完成一次支付,但攻击者通过修改参数、跳过程序、重复触发等手段,让系统最终按自己的期望完成交易。支付系统是典型的多方联动:浏览器、业务服务器、支付网关、数据库,每一跳通信都涉及参数传递和状态判断,任何一环只要“太信任对方”,就可能被钻空子。

为什么这类漏洞如此普遍?我的体会有三点。第一,支付流程的业务状态机远比想的复杂,很多团队对“待支付、已支付、已退款、已关闭”这些状态的定义都含混不清;第二,大部分后台开发在写支付模块时,默认前端提交的数据是可信的,因为接口测试时他们自己总传正确值;第三,开发周期紧,安全测试又容易只盯着注入、越权这些“经典科目”,偏偏支付逻辑属于业务安全范围,很多测试人员缺少拆解业务的习惯。

1.2 八大姿势的整体分类与上下篇规划

为了不一次性信息过载,我按“攻击者在支付链路中涉及的操纵对象”把问题分成八大姿态。上篇这四类集中在“请求参数和信任模型”:

  • 金额篡改:直接改价格、数量、金额标识,期望后端拿错了值去结算;
  • 优惠券与积分滥用:利用券码规则不严、叠加逻辑缺陷或积分流转漏洞,达到超额抵扣;
  • 支付回调篡改:哄骗业务后端相信“支付已成功”,实际上没有走完第三方支付流程;
  • 越权订单操作:通过修改订单号、用户标识、店铺标识等对象引用,操作他人的订单或支付记录。

下篇计划重点梳理竞态条件(重复下单并发支付)、支付流程绕过(从前端Step跳Step)、汇率精度与单位陷阱(分转元、外币折算、进位回滚),以及重放攻击(对回调、对支付凭证、对退款接口)。这四类都偏“行为时序”和“状态机”方向,和上篇的“参数信任问题”正好互补。

下面正式进入上篇的逐个拆解。

2. 第一个姿势:金额篡改,最朴素也最难防

2.1 攻击者到底在改什么

金额篡改听着很“初级”,但现在依然大量残留在业务系统中。最典型的是直接篡改下单接口的金额字段。举个我见过的简化样例,前端下单提交的是类似下面这样的JSON:

{ "productId": 10231, "productName": "企业版会员年卡", "price": 499.00, "quantity": 1, "totalAmount": 499.00, "userId": 88991 }

如果后端只校验productId存在、库存足够,就直接把totalAmount落库并带入收银台,那攻击者把这个字段改成0.01,支付环节就会按照0.01生成订单。更聪明的做法是保留price不变,只改quantity为负数,比如-1,在某些未校验数量非负的系统里,总价反而会变成负的,最后用户支付金额为负,相当于平台倒贴钱。另一种常见操作是修改商品标识,把商品A的高价换成商品B的低价再下单,只要同名商品的校验不严格,也会成功。

这里有个容易让人产生误区的地方:很多开发者以为只要“前端只读展示,不让用户改”,就算安全。实际情况完全不同,任何HTTP请求都可以被抓包工具抓下来再改掉,前端限制只能防君子,防不了有心人。金额篡改的核心漏洞点永远只有一个——后端是否基于可信数据源重新计算订单金额

2.2 我实际遇到的几个变体场景

现实中,金额篡改不是只发生在下单接口,还有几个变体特别值得注意。一个是“换购活动”场景,A商品原价100元,参加换购后可以10元加购B商品,后端把加购价格作为参数传到了接口里,攻击者把“加购价”参数改成0,直接免费拿B。另一个是“运费计算”场景,很多系统的运费模板在用户侧动态计算,然后把运费金额提交到后端,我把运费改成0,就能一直包邮,这样薅得不多,但如果叠加批量刷单,损失也可观。

还有一类更隐蔽,出在“退款申请”接口上。正常流程是用户对已支付订单发起退款,后端应读取数据库里的实付金额,退回给用户。但我见过部分系统的退款接口把退款金额放在请求体里,由用户提交,这就意味着用户在订单实付499的情况下,可以申请退款999。虽然很多网关对退款金额有校验,但部分银行直连或第三方机构只做粗略审核,风险依然存在。

2.3 金额篡改类问题的排查与修复思路

排查这类问题其实有固定套路。备好一套测试环境,把所有涉及金额读写的位置列出来:购物车结算、下单生成订单、优惠后应付款、支付回调金额落库、退款金额计算、充值赠送金额计算,逐一对接口发起合法和非法请求,重点观察是否有任何一处“金额来自请求参数且未经服务端二次计算”。

修复方案上,我的核心建议是把金额数据的权威来源锁定在服务端。用户提交的金额只作为“参考展示”,最终计算都要基于数据库内商品原价、服务端下发的折扣、状态机里记录的优惠明细重新执行。数量参数必须做强校验,必须为正整数,超出合理上限要做拦截和人工审核。价格、优惠、折扣这些关键字段,即使前端不传,后端也要有能力重新推导。审计日志里要对金额变异做记录,一旦检测到提交金额与重算金额不一致,立刻告警并标记风控。

注意:线上环境千万别直接拿真实订单去试“修改金额再支付”,哪怕是发起1分钱支付都可能触发支付路由问题。所有触发类测试都应在带独立交易凭据的联调环境里完成。

3. 第二个姿势:优惠券与积分滥用,薅羊毛的最高发地带

3.1 优惠券滥用的常见拆解

优惠券系统被薅的案例,简直可以单独写一本书。就上篇的篇幅,我想讲三个高发的切入点:券本身的绑定关系、券码的生成规则不可枚举、以及优惠叠加逻辑的顺序错误。

先说绑定关系。正常的券发放应该绑定user_id,并且只能由该用户使用。但有的系统在核销接口上只校验“券码是否存在”,不校验“券码归属者是否等于下单用户”,攻击者只需拿到别人的券码字符串就能使用。更严重的是,有些券码是自增ID或日期拼上随机数,随机数位数太少,可以被枚举出来。我在一个优惠活动中见过6位纯数字券码,脚本一跑,一天就能遍历出几十张未使用的大额券。

再说叠加逻辑。满足优惠条件时,系统往往是先算单品折扣,再算满减,再算平台券,再算店铺券。顺序错了,会出现“折扣叠加让金额变成负数”的尴尬情况。比如一个商品原价100,店铺满100减50,平台又发了一张满50再减50的券,理论上剩余金额应该是0,但由于先减平台券再减店铺券,导致应付款变成0,甚至出现负数后系统直接放行,订单金额为0。

3.2 积分系统的逻辑坑

积分和优惠券本质相似,区别是积分更像一种“虚拟货币”,流通链路更长:获得、冻结、结算、扣减、过期。常见漏洞主要集中在三点。

第一,积分扣减与订单创建解耦。用户下单时积分余额足够,直接扣减,但订单支付失败后,积分没有回滚,等于积分凭空消失。反向的漏洞更值钱——用户发起退款,系统只退积分不退钱,或者既退积分又退钱。第二,积分抵扣比例可改。有些系统中抵扣金额的计算公式在前端完成,比如前端计算出“消耗1000积分,抵扣10元”,后端只接收结果不重新算比例,攻击者绕过前端直接提交“消耗1000积分,抵扣1000元”。第三,负积分操作。如果积分的增加和扣减分开两个接口,而增加侧没有做“是否正在支付/是否可正常扣减”的状态校验,攻击者可能通过先扣后用、再用负值冲销的方式,把积分余额刷成天文数字。

3.3 优惠与积分的审计建议

优惠券审计时,我建议优先检查四件事:券码生成是否具备足够熵且不可枚举;核销接口是否同时校验券归属、券状态、订单金额满减条件;优惠活动是否限制单用户领取数量和使用频率;叠加顺序是否固定且有一个统一的金额计算服务,而不是分散在每个前端或接口里各自算。

积分系统审计时,要围绕整个生命周期画一条完整的状态流转,重点关注“扣减失败是否回滚”“订单取消后是否返还”“退款场景下钱和积分是否走同一套冲正逻辑”。再有就是所有积分变动必须能追溯,不能用“积分余额变化了但看不到日志”的方式,否则排查问题时根本无从下手。

4. 第三个姿势:支付回调篡改,信任假象下的重灾区

4.1 回调机制为什么会成为逻辑漏洞温床

在线支付里,用户在前端点击“去付款”,会被引导到第三方支付页面,完成后第三方支付平台再异步通知业务服务器,服务器收到通知后把订单置为“已支付”,这就是回调机制的正常流程。问题是,很多团队把“第三方平台的回调通知”和“用户浏览器的跳转结果”混为一谈。用户完成支付后,支付平台通常会同时做两件事:在用户浏览器上跳转一个同步通知地址,再向后台服务器发一条异步通知。如果开发者把同步通知URL当作可信依据,攻击者完全可以自己构造一条“支付成功”的同步跳转请求,引导服务器去验收订单。

异步通知如果没做好验签,就更麻烦了。回调通知报文一般包含订单号、支付金额、支付状态、交易流水号等字段,如果后端没有严格校验签名,攻击者直接伪造一条异步通知,说“这个订单已经支付成功”,服务器就会被骗。我见过最离谱的案例,回调接口的验签逻辑写成了“当前环境为测试环境时跳过验签”,结果测试环境直接部署到了公网可访问的域名上,攻击者随手改了个域名就绕过了全部校验。

4.2 回调篡改的核心攻击形态

我总结了一下,回调篡改的常见形态大致有四种:

  • 伪造成功通知:不经过真实支付,直接给业务后端发送一条伪装的支付成功通知;
  • 参数替换:真实订单号写的是A,攻击者把通知里的订单号改成B,让B订单也被置为已支付;
  • 金额低配:真实支付金额1元,但通知里写金额100元,如果后端没有校验“通知金额是否等于订单金额”,就会造成订单金额和实收金额不一致;
  • 重放攻击:将一次真实的成功通知反复发送,如果后端对回调通知没有做去重和幂等校验,同一个订单可以被多次置为成功,或多次触发发货、多次触发积分发放。

这四种形态里,参数替换和重放是实战中影响面最大的。参数替换尤其在“订单号可用简单数字递增”的系统里风险极高,攻击者通过遍历订单号,可以把一批订单全部置为已支付。重放攻击则容易发生在“回调处理逻辑不具备幂等性”的系统,明明订单已经处理过,重复进来的回调又执行了一遍发卡或加余额的动作。

4.3 回调接口的正确实现方式和自查手段

关于回调接口,我习惯用一句话去要求团队:回调接口必须视为完全不可信的入口,所有业务动作都要基于签名验证和服务端重查,而不是基于报文内容

具体到实现上,有五个关键检查点。第一,验签必须基于私钥或密钥对称加密,且密钥在服务端保存,前端不可获取。第二,验签通过后,还要把“通知金额、订单号、商户号”与业务系统里缓存的下单信息做严格比对。第三,处理逻辑必须幂等,可以用数据库唯一约束或Redis分布式锁,保证同一笔订单的回调不会触发重复的发货、发凭证、加余额动作。第四,通知处理结果应有明确的响应值,比如按第三方文档要求返回“success”或“fail”,未处理成功时让支付平台按策略重试,而不是在本地静默吞掉异常。第五,要有独立的回调监控表,记录每次通知的原始报文、验签结果、处理结果,线上问题排查时一搜便知。

自查时,我通常会做三组模拟测试:伪造一条未签名的成功通知看是否会被接受;复制一条真实通知改掉订单号/金额看后端是否发现;把同一条通知连续发送三次,看订单状态和发券记录是否有异常。

5. 第四个姿势:越权订单操作,隔壁订单好“好香”

5.1 IDOR在支付场景里的具体表现

越权这个词大家都熟悉,但在支付场景里它经常被忽略,因为大家总觉得支付就是“我自己付自己的钱”,哪有什么越权可言。其实支付链路里的越权形式非常丰富,主要是对订单号、退款单号、用户ID、店铺ID这类对象标识的越权访问和越权操作。

典型场景一:查看订单。很多系统的订单查询接口只校验登录态,不校验订单归属,登录用户A的登录态下,只要把orderId改成任意数字,就能查看到其他用户订单的收货地址、手机号、商品清单甚至支付流水。这在黑产里是一条非常值钱的“信息收集”通道,用户的联系人姓名、住址、手机号都会从这类接口里批量泄露。

典型场景二:操作订单状态。我曾见过一个系统,退款申请接口只凭一个退款单号就能直接触发原路退还,不校验申请人和原订单是否有关联。换句话说,只要我知道了别人的订单号和退款单号,就能帮别人发起退款,至于退到谁的支付账户,自然是原支付账户,所以这个更像“帮别人免费退货”,真正危害大的场景是:攻击者使用自己的支付账户完成支付,然后利用越权把退款状态改成已退款,却把退款金额转移给自己——这需要系统把“退款人账户”和“原支付账户”结算通道分开,现实中确实存在这种设计缺陷。

典型场景三:店铺或商户维度越权。多商户电商平台里,一个商户如果能够通过修改商户ID把其他商户的结算单、提现申请置为已结算、已通过,资金的损失就是平台级别的。这类漏洞往往藏得特别深,因为普通测试只关注用户端,没人会去关注商户后台的对象级鉴权。

5.2 越权问题的根因与修复分层

越权问题说白了就是对象级授权缺失,后端在处理资源标识符时,没有先校验“当前登录用户是否是该资源的所有者/被授权人”。修复时建议分三层去补:

  • 第一层,资源访问前先鉴权,每个涉及订单、退款单、优惠券、积分余额的接口,都必须拿到当前登录态,再校验登录态中的uid和资源归属方的uid是否一致;
  • 第二层,对象标识尽量不暴露可猜测的自增ID,对外统一用不可枚举的随机串或UUID,但同时要注意,光换标识符不够,内部逻辑仍要做归属校验,因为隐藏ID只能增大猜测难度,不能替代鉴权;
  • 第三层,所有变更状态类操作必须补充操作审计,记录“谁在什么时间把哪个订单从什么状态改成了什么状态”,一旦出现越权事件,要靠审计数据做倒查。

排查时,我的做法是“横向测越权+纵向测提权”。横向就是登录两个普通账号,互相拿对方订单号去访问、去操作,重点放在退款、确认收货、取消订单这几种高风险动作上。纵向则模拟一个普通用户尝试操作后台管理接口的订单确认、发货处理,看看是否存在权限校验缺失。

经验提醒:越权测试最忌讳“只截一张图就完事”。一个接口存在越权,往往意味着同类接口都可能有类似问题。我一般会先把业务系统的接口清单拉出来,按照“查询类、变更类、列表类”分组,批量跑一遍鉴权逻辑,这样既能提高覆盖度,也方便写复现报告,让开发一次改到位。

6. 上篇实战清单与排查技巧实录

6.1 一份可以直接用的支付逻辑自查清单

很多读者看完上面的梳理,可能还是会问:我手上正好有一个支付项目,到底该怎么测?我把上篇提到的四类问题整理成了一套自查清单,建议按顺序执行,每一条都要有明确的通过标准。

  • 金额来源核查:下单接口、收银台接口、退款接口最终使用的金额,是否都来自服务端数据库重算,而不是直接取前端参数;
  • 数量强校验:商品数量、运费数量、优惠券数量是否都做了正整数校验和上限限制;
  • 优惠券归属校验:核销接口是否校验券归属、券状态、使用次数、活动有效期、使用门槛;
  • 优惠叠加顺序:满减、折扣、平台券的叠加是否有统一的服务端计算服务,会不会出现实付金额为负数或0的情况;
  • 积分扣减一致性:积分扣减和订单状态是否在同一事务里,取消订单、支付失败、退款时能否正确回滚;
  • 回调签名与对账:回调接口是否验签、是否对金额和订单号做二次匹配、是否具备幂等控制;
  • 回调重放防护:重复通知同一订单时,数据库状态和外部动作不会重复执行;
  • 订单归属鉴权:所有订单、退款单、物流单的查询和操作接口,是否先校验当前登录用户与资源归属方一致;
  • 对象标识强度:订单号、退款单号等资源ID是否使用不可猜测的乱序标识;
  • 审计日志覆盖:资金类接口是否记录了完整的操作前后日志,方便在突发事故时定位。

按这套清单过一轮,大部分基础性支付逻辑漏洞都能暴露出来。如果你发现某一条实测不通过,别急着让开发改,建议先把“触发链路”用一条图式写清楚:谁发起请求、修改了哪个参数、后端哪里没校验、最终落库什么状态,这样的描述对开发定位问题会非常高效。

6.2 我踩过的坑和总结的方法论

几次支付项目测下来,我真切体会到三件事。

第一,支付逻辑测试最忌“只测正常流程”。开发自测时通常只会走一遍成功支付路径,安全测试如果也按这个思路走,基本测不出问题。真正要下功夫的是异常路径顺序调整:支付成功后改了金额再付一次、创建订单后不支付直接请求发货、退款申请审核通过后重新发起支付……每一条异常路径都可能隐藏着状态机漏洞。

第二,日志和监控是支付安全里的隐形保命符。很多团队上线后才发现被刷单,最大的难点不是漏洞本身,而是根本查不清攻击者什么时候进来、改了什么、影响了多少订单。如果日志里连请求参数、验签结果、回调报文都没记录,那整个复盘就会变得非常痛苦。所以我在支付模块的性能压测和上线评审里,永远把日志完整性放在功能前。

第三,上篇聊的这四类问题全都能靠“重新计算”来解决。核心不是禁止用户传参数,而是让后端在用户提交之后再独立算一遍,拿算出来的值覆盖客户端传来的值。谁算,谁就拥有最终话语权。这个思路贯穿支付安全始终,下篇会继续沿着它去展开竞态条件、流程绕过、精度和重放这些更隐蔽的姿势。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询