:金额篡改、优惠券滥用、回调篡改与越权订单)
在接收支付相关的安全测试任务时我最常说的一句话是搞业务逻辑漏洞拼的不是手里有多少高深的扫描器而是能不能顺着业务流转的链路把每个环节的“信任点”逐一挑出来盘问一遍。信任点越隐蔽往往越危险。支付这类强业务、强交互、强资金属性的功能天然就是逻辑漏洞的重灾区——开发在赶版本时最容易顺手写下“客户端传入什么后端就信什么”的代码。这类问题埋得深、触发面广自动化工具经常扫不出来但一旦被人盯上轻则刷单薅羊毛重则资金被批量转空。中途接手过一套电商系统的安全复审之后我才意识到支付链路的坑远不止“改个价格”那么简单。网上关于支付逻辑漏洞的盘点不少但大多是零散案例堆砌。我把这些年见到的、实测过的、以及同行分享过的典型问题归纳成八个大类准备分上下两篇来讲。上篇先聊前四类金额篡改、优惠券与积分滥用、支付回调篡改、越权订单操作。这四类共同点是“攻击者手里能改的参数特别多”非常适合理清支付链路的基本信任边界。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 我踩过的坑和总结的方法论几次支付项目测下来我真切体会到三件事。第一支付逻辑测试最忌“只测正常流程”。开发自测时通常只会走一遍成功支付路径安全测试如果也按这个思路走基本测不出问题。真正要下功夫的是异常路径顺序调整支付成功后改了金额再付一次、创建订单后不支付直接请求发货、退款申请审核通过后重新发起支付……每一条异常路径都可能隐藏着状态机漏洞。第二日志和监控是支付安全里的隐形保命符。很多团队上线后才发现被刷单最大的难点不是漏洞本身而是根本查不清攻击者什么时候进来、改了什么、影响了多少订单。如果日志里连请求参数、验签结果、回调报文都没记录那整个复盘就会变得非常痛苦。所以我在支付模块的性能压测和上线评审里永远把日志完整性放在功能前。第三上篇聊的这四类问题全都能靠“重新计算”来解决。核心不是禁止用户传参数而是让后端在用户提交之后再独立算一遍拿算出来的值覆盖客户端传来的值。谁算谁就拥有最终话语权。这个思路贯穿支付安全始终下篇会继续沿着它去展开竞态条件、流程绕过、精度和重放这些更隐蔽的姿势。