
你有没有遇到过这种情况功能全部正常扫描器跑完一轮一个高危都没有但某笔订单就是能被改成 0 元某个优惠券能无限次使用某个普通用户能把自己变成管理员。如果有那你已经碰到了业务逻辑漏洞。业务逻辑漏洞是 PortSwigger 在 Web 安全体系里反复强调的一类问题它不靠注入、不靠绕过 WAF而是靠“合规地使用功能”来打破规则。本文把这类漏洞拆成 4 种攻击模型分别讲透它们的成因、复现思路、检测方法和修复思路。想认真做渗透测试、安全开发或者准备相关认证的读者这篇文章值得收藏后慢慢对照。1. 为什么说业务逻辑漏洞是“打破规则”的攻击1.1 业务逻辑漏洞与传统漏洞的分水岭传统漏洞比如 SQL 注入和 XSS本质是“输入没有被当作数据”或“输出没有被转义”。攻击者发送的是畸形输入系统产生了未预期的行为。业务逻辑漏洞却完全不一样攻击者发送的请求本身是合法的参数类型对、格式对、甚至权限也对但组合起来的业务流程却违反了业务规则。打个比方SQL 注入是有人从窗户爬进银行金库业务逻辑漏洞是有人拿着真实存折、按正常流程在柜台取走了别人账户里的钱。柜员没有识别出操作者的身份和授权这就是流程设计的问题。PortSwigger 在大量漏洞实验里反复传递一个观点业务逻辑漏洞的关键在于理解开发者预设的“正常流程”。扫描器不懂商业规则它不知道一张满 300 减 50 的券在一个订单里只能使用一次也不知道下单时必须经过支付确认。因此这类漏洞只能靠人工去分析、去推演。1.2 为什么自动化扫描器扫不出这类漏洞漏洞扫描器的工作方式是发大量探测请求然后基于响应特征做匹配。遇到 SQL 注入它可以看到数据库报错遇到反射型 XSS它可以在响应里找到脚本载荷。但业务逻辑漏洞没有统一的“漏洞特征”。比如购物车接口接收了一个负数商品数量服务端又用这个负数直接参与计算最终订单总金额变成负数。扫描器会认为这个请求“成功返回 200响应结构正常”它不会知道 -1 件商品在正常业务里根本不应该出现。再比如优惠券接口扫描器可以反复提交同一个兑换码但它不知道业务规则要求“每个用户只能兑换一次”。这就是为什么 OWASP 这些年一直把业务逻辑漏洞列入重点关注对象。它考验的不是抓包工具用得多熟练而是测试者对业务规则的理解深度。安全测试做到最后拼的往往是“你有多懂这家公司的业务”。1.3 4 种攻击模型的整体地图PortSwigger 的业务逻辑漏洞实验涉及场景很多但归归类攻击手法基本围绕以下 4 种模型展开。攻击模型核心思路典型场景过度信任客户端控制服务端完全相信前端传过来的业务参数前端提交价格、折扣率、积分、权限标识流程状态机被跳过业务流程分步执行但服务端只校验当前步骤不校验前置状态跳过验证码直接改密码、跳过支付直接完成订单双重用途接口隔离不严同一个接口同时服务普通用户和管理功能授权校验不完整普通用户通过隐藏参数提升角色、修改他人数据边界条件与可预测数据数值边界、并发状态、可枚举标识被滥用负数量、整数溢出、折扣码并发使用、订单号越权下面每一节都对应一种模型我会结合自己实际测试中的案例讲不会只贴概念。2. 攻击模型一过度信任客户端控制——改个参数就能白嫖2.1 这个漏洞的本质和触发条件过度信任客户端控制的直接成因是前端把业务判断当成安全判断。最常见的形式就是下单请求里带着价格字段。开发者为了方便在前端计算出商品总价然后把这个总价随请求提交给服务端。服务端没有再查数据库里的单价直接信任请求里的金额于是整个价格体系就被架空了。我早期测某个演示商城时发现购物车请求是长这样的POST /api/cart/checkout HTTP/1.1 Host: example.com Content-Type: application/json Cookie: sessionabc123 {items:[{id:1001,name:演示商品,price:199.00,quantity:1}],total:199.00}这里有两个危险信号一个是price在前端可控另一个是服务端拿着前端算好的total去做支付。只要把price改成 0.01或者直接把total改成 0.01支付金额就会跟着变。如果连支付接口都不校验订单状态后面就是 0 元购。除了价格这类模型还经常出现在折扣率、积分、优惠金额、会员等级、权限角色等字段上。有些站点比较隐晦不会直接暴露price而是暴露discountRate或pointMultiplier但本质一模一样任何业务里“由前端算好再提交”的数值都有被改的风险。2.2 复现路径从抓包到验证要复现这类漏洞不要急着改参数先把正常流程抓清楚。我用 Burp Suite 的流程一般是这几步用浏览器正常走一遍“加入购物车→进入结算→确认订单”的完整流程确保每一步请求都被代理记录下来。在 HTTP 历史里找到携带商品金额或优惠信息的请求重点看 POST 和 PUT 请求的 JSON 参数。把目标请求发送到 Repeater修改可疑字段比如把price改成0.01。观察响应。如果响应里回显了修改后的金额并且后续支付页面也按这个金额走基本就实锤了。再验证一次服务端是否重新计算把quantity改成 2但price保持 0.01看最终总额是按 0.01×2 算还是按数据库里的 199×2 算。下面是我在实际测试中记录的参数对比方便大家理解“为什么响应能证明漏洞存在”。请求参数正常值篡改值服务端响应price199.000.01订单总额 0.01无任何重新计算quantity15订单总额 5×0.010.05discount0100实付金额从 199 变成 99从响应可以看出服务端完全没有使用数据库里的商品单价也没有校验折扣上限。这种情况不需要再构造更复杂的攻击链直接影响就是任意金额下单。2.3 修复思路修复的核心原则是业务关键数据必须由服务端统一计算前端参数只能作为引用不能作为依据。商品单价、库存、折扣上限、税费等必须从数据库或配置中心读取。前端传上来的total、price、discount字段一律忽略或者只把它当作展示用。如果一定要支持前端传优惠金额服务端必须校验规则比如“折扣不超过订单总额的 20%”“优惠券与用户绑定”等。补一句实操心得测试这种漏洞时要顺手多试试“把价格字段删除或者改成 null”。有的站服务端会判断“如果没有传 price就用数据库价格”但如果传了 null反而会因为弱比较逻辑走到另一个分支。这种边角越反直觉越容易出问题。3. 攻击模型二流程状态机被跳过——验证码、支付步骤只是摆设3.1 漏洞成因缺少状态流转校验现实中的业务流程基本都是分步的下单先选商品再填地址然后支付最后确认。开发者写代码时往往只关注“当前这一步的入参是否合法”却忽略“当前这一步是否应该被访问”。就拿找回密码流程来说正常步骤是输入用户名。验证绑定手机号并输入验证码。设置新密码。如果服务端在第 3 步只检查“新密码格式是否正确”不检查“当前用户是否已经完成了第 2 步”攻击者就可以跳过验证码校验直接构造第 3 步的请求把任意账户的密码改成自己知道的密码。这就是流程状态机缺失。我用 PortSwigger 的分类习惯来理解这类漏洞的本质是“步骤的执行顺序没有被服务端记录”。服务端拿到了最终状态的请求却没有验证前置状态是否满足。3.2 复现思路先走正常流程再构造非法序列复现这类漏洞最忌讳上来就乱发请求。正确做法是先把正常流程的每个请求按顺序记录然后观察哪些参数可以控制步骤状态。我常用的方法走一遍正常流程用 Burp Suite 记录所有请求的顺序。分析每个请求里的字段重点关注step、phase、status、next、action这类和流程位置相关的参数。用 Repeater 尝试“调换步骤”比如正常是 A→B→C试着直接发送 C或者发送 A→C。为了排除杂音可以在 B 步骤里故意填一个错误验证码看 C 步骤是否仍然能执行成功。举一个典型的请求示例。假设修改密码的最后一步请求是POST /api/user/password/reset HTTP/1.1 Host: example.com Content-Type: application/json Cookie: sessionabc123 {username:victim,newPassword:Hacker123,step:3}如果服务端只校验step3的格式而没有校验“会话中已经完成了短信验证码校验”那这个请求可能直接成功。此时用户完全不知道自己的密码已经被重置。3.3 怎么区分误报和真漏洞这类漏洞很容易出现“看着像实际是误报”的情况。有一个关键判断点成功标准不是响应码而是业务状态是否真的变化。比如你发送了上面的请求响应返回{code:0,msg:success}但随后用新密码去登录却失败说明服务端其实还是做了前置校验只是响应文案没做区分。相反如果你能直接用新密码登录目标账户那才是实锤。另外要注意验证码本身可能只是“前端生成、服务端不验证”的假验证码。测试时我会故意提交一个完全错误的验证码如果后续步骤仍然受理说明验证码校验根本不存在。这在很多老旧系统里非常常见。3.4 修复思路服务端用会话状态机保存每一步的完成状态比如session[email_verified]true只有前置状态满足才放行下一步。关键操作如改密码、改邮箱、支付、提现必须在执行前重新校验身份。流程标识step不能由前端传入最好由服务端根据会话推导。验证码校验必须走服务端接口不能只在前端用 JS 比较。我再补充一个收益很高的测试点凡是“提交最终结果”的接口把请求里的step改成任意值、删掉step、把step改成超出范围的数字都可能暴露不同的分支逻辑。多试几种畸形状态往往能撞开流程闸门。4. 攻击模型三双重用途接口隔离不严——一个接口管两套权限4.1 漏洞成因同一个端点两种信任级别双重用途接口问题可以简单理解为“一个接口既为普通用户服务也为管理员服务但授权校验只覆盖了其中一条路径”。最常见的情况是后台管理功能和前台用户功能共用了同一个接口但主要区别仅体现在请求参数上。比如一个更新用户资料的接口POST /api/user/update HTTP/1.1 Host: example.com Content-Type: application/json Cookie: sessionabc123 {nickname:test,role:admin}正常用户操作只会提交nickname和avatar但接口后端代码可能直接把请求体里的所有字段解析出来更新到用户表。攻击者多提交一个role: admin角色就被改了。开发者想的是“内部接口只给管理员调用”写代码时却为了省事复用了同一个入口把希望寄托在“前端不会传这些字段”上。PortSwigger 的实验把这类问题归为 dual-use endpoints强调的不只是接口复用还包括“同一个对象、不同权限”的访问控制。比如订单详情接口同时支持普通用户查看自己订单、管理员查看任意订单但后端没有按角色区分数据范围就会演变成越权。4.2 复现思路抓包时不要只看参数名字更要看接口语义我测这类漏洞时会先把接口清单收集起来然后按“功能角色”分两类一类是当前用户能调用的另一类是文档或源码里暗示存在但前端没暴露的。接着逐个检查普通用户接口是否支持传递权限相关字段如role、isAdmin、group_id。同一个 URL 是否支持不同的 HTTP 方法比如GET /api/order/1和DELETE /api/order/1可能分别走不同的处理器。是否存在“隐藏参数”能让普通请求走到管理员分支比如debug1、internaltrue。我遇到过一个真实例子一个商城的订单查询接口普通用户通过GET /api/order/{orderId}查询自己的订单返回的 JSON 里带了一个sellerId。如果把sellerId改成别人的 ID接口居然返回了那个卖家的所有订单数据。这个数据字段既是业务字段又是权限判断的依据但服务端只在读订单主记录时做了用户校验没有在返回子列表时再做一次过滤。还有一些系统会把管理操作放在同样的路径下只是加了个隐藏参数POST /api/product/update HTTP/1.1 Host: example.com Content-Type: application/json Cookie: sessionabc123 {productId:88,price:0.01,admin:true}如果后端真的读了admin字段那普通用户就能改商品价格。检测方法很简单先以普通用户请求不带admin的版本再带上admin: true重放比较响应差异。4.3 修复思路与测试注意点修复的核心是“接口职责单一化 服务端显式授权”。不要用同一个函数处理普通用户请求和管理员请求至少要拆成两个独立的处理逻辑。删除所有“隐藏参数当开关”的做法权限判断必须基于会话里的角色而不是请求体里的字段。对对象级别的访问要同时校验“用户是否有权访问该对象”和“用户是否有权执行该操作”。后端接口要进行字段白名单绑定比如更新用户资料时只更新指定字段其他字段一律忽略。测试注意点这类漏洞通常隐藏很深因为开发者会刻意把参数名做得不直观。我的习惯是在 Burp Suite 的 HTTP 历史里搜索role、admin、level、type、permission这些关键词再逐一在请求里追加试错。另外抓到的每个列表接口都要考虑“换 ID 看看”因为双重用途接口经常伴随 IDOR。5. 攻击模型四边界条件与可预测数据——负数、溢出、折扣码复用5.1 漏洞成因业务规则没有覆盖极端输入这一类的共同点是开发者在写逻辑时只考虑了正常范围内的数据没有考虑边界条件和并发状态。攻击者不需要绕过任何校验只需要把值调整到业务规则没覆盖到的区间。先说数值边界。电商下单时服务端往往只校验商品数量“是否大于 0”却没有校验“是否小于某个上限”。如果把数量改成 -1某些系统里总金额就会变成负数。更离谱的是负数金额可能触发“退款”逻辑攻击者不仅白拿商品还能倒赚钱。数量溢出也很典型。假设服务端用 32 位整型接收数量正常上限是 2147483647。攻击者提交一个 2147483648因为溢出会被解释成 -2147483648进而绕过数量校验。或者单价很高、数量很大时总价超过整型上限回绕成一个小正数最终实付几块钱。再说可预测数据。很多系统生成订单号、优惠券编号、用户 ID 时采用连续整数。攻击者只要遍历订单号就能看到不属于自己的订单信息。这类漏洞不一定造成直接资金损失但信息泄露同样高危。最后是并发状态。一个优惠券“每用户限用一次”的校验如果只在用户点击时检查“是否已使用”而没有加锁或幂等控制两个并发请求可能同时通过检查同时把优惠券用掉。5.2 复现路径与关键参数计算复现边界问题时我会准备一组边界值清单按顺序测试数量0、-1、1、最大值、最大值1、最小值-1金额0、0.01、负数、极大正数字符串长度超长字符串、空字符串、单字符优惠券同一个券重复提交 N 次、并发提交 N 次用一个下单接口举例。假设服务端从数据库查出来的商品单价是 199 元而请求里的数量是-1POST /api/order/create HTTP/1.1 Host: example.com Content-Type: application/json Cookie: sessionabc123 {productId:1001,quantity:-1}如果服务端没有对负数做拦截并且总金额是199 × (-1) -199那么订单就会产生一个 -199 元的负数金额。后续如果系统有余额充值逻辑这个负数可能会被当成“可用余额”抵扣其他订单。对于整数溢出需要先了解服务端语言的数值类型。如果是 Java 或 C# 的 32 位 int可以试2147483647和2147483648如果是 JavaScript整数是安全的但超过Number.MAX_SAFE_INTEGER后精度就会丢失。测试前先通过响应报文里的错误提示判断语言环境。可预测数据的测试更简单。用 Burp Suite 抓一个查询订单的请求GET /api/order/10001 HTTP/1.1 Host: example.com Cookie: sessionabc123然后依次把 10001 改成 10000、9999、10002。如果响应里出现他人的订单信息那就是越权。有些系统会用 UUID 或哈希订单号那就不要硬试应该找逻辑缺陷而不是直接爆破。5.3 修复思路商品数量、金额等字段必须设置上下限并在服务端统一校验比如“数量必须大于 0 且小于等于 99”。涉及金额计算时使用高精度类型或定点数避免浮点误差和整数溢出。优惠券、签到、抽奖等“限量执行”的业务必须有原子操作或分布式锁防止并发重复使用。订单号、用户 ID 等不应对用户暴露连续可枚举的标识尽量使用随机数或哈希化 ID。每个查询接口都要按登录用户的权限做数据范围过滤。顺带提一个实用小技巧判断边界值是否被真正信任可以看响应里的“回显数据”。如果响应把数量、单价、总额都原样回显说明服务端直接用了请求里的值如果回显的值和请求不一致说明服务端可能重新计算了这时候再往下测只是浪费时间。6. 实操用 Burp Suite 从零复现一次业务逻辑漏洞测试6.1 准备工作靶场选择与代理配置业务逻辑漏洞测试需要完整的业务环境不适合直接在公网目标上乱改。我建议先在本地搭一个演示商城或者用 PortSwigger 平台上的专用靶场来做验证。靶场的优势是每个实验都有明确的通关标准方便确认漏洞是否真实存在。代理配置是老生常谈但我要强调三点浏览器代理指向 Burp Suite 的监听端口比如127.0.0.1:8080。需要抓 HTTPS 流量时提前导入 Burp Suite 的 CA 证书。开启“代理拦截”前先确认浏览器插件或者系统代理设置是否正确否则容易出现抓不到包的情况。准备一个靶场后不要急着乱点先注册两个账号一个普通用户、一个测试目标用户。两个账号的对比是验证越权和逻辑绕过的基础。6.2 完整测试闭环从“改价格”开始假设靶场是一个商品商城。我以“篡改价格”作为完整测试闭环示例步骤如下。第一步正常购买流程。登录普通用户账号访问商品详情页加入购物车进入结算页面提交订单。过程中记录所有 HTTP 请求。第二步定位关键参数。在 HTTP 历史里找到POST /api/cart/checkout请求发现请求体里有以下字段POST /api/cart/checkout HTTP/1.1 Host: example.com Content-Type: application/json Cookie: sessionuser1 {cartId:cart-123,items:[{id:1001,name:商品A,price:199.00,quantity:1}],total:199.00}第三步修改参数。把price和total都改成 0.01发送请求。响应返回HTTP/1.1 200 OK Content-Type: application/json {orderId:order-56789,total:0.01,status:pending}第四步验证结果。进入收银台看到订单金额确实是 0.01 元。用实际支付渠道模拟支付一分钱后订单状态变成“待发货”。到这里就可以确认服务端完全信任了前端金额。第五步记录证据。我会把正常请求、篡改请求、篡改后的订单页面截图放在一起作为漏洞报告附件。报告中会说明影响的资金范围以及修复建议。6.3 测试过程中必看的 5 个位置业务逻辑漏洞不会自己跳出来要主动在关键位置寻找。我整理了 5 个高频位置位置排查重点购物车与结算接口价格、数量、折扣、运费是否由前端传入密码找回与注册接口步骤参数是否可控验证码是否真的被服务端校验用户资料更新接口是否存在角色、权限、余额等隐藏字段优惠券与积分接口是否限制使用次数、是否校验归属、是否可并发订单查询接口订单号是否可枚举是否校验订单归属这 5 个位置不只是测试入门时的清单也是每次做逻辑测试前必过的基线。它们基本覆盖了 4 种攻击模型的主要落地场景。6.4 实操中的经验不要只盯响应状态码很多新手看到 200 就认为是漏洞看到 403 就认为不可能。业务逻辑漏洞的证明逻辑是“状态变化”不是“响应代码”。最好在每测完一个可疑点后回到业务页面刷新一下看页面里的数据是否真的变了。举个例子你把商品价格改成 0.01接口返回 200但回到详情页一看价格还是 199说明服务端只是“收下了请求但没当真”。这时候不要急着报告再试几个绕过方式比如加一个updateParamstrue的参数或者把商品 ID 换成库存少的 SKU。有些系统只在特定场景下重新计算价格。7. 常见问题与排查技巧实录7.1 常见问题速查表我在测试业务逻辑漏洞时经常遇到一些重复性问题。这里整理成一张速查表方便大家对照排查。现象可能原因进一步验证方法改了价格但页面没变服务端对价格字段做了二次计算删除price字段后重发看服务端是否用数据库价格改了角色但权限没变角色来自会话而非请求体重新登录后再查看角色值确认是否被持久化数量溢出不生效语言类型不是整型或服务端做了范围限制尝试超长字符串、极大浮点数寻找其他边界路径请求返回 302 到登录页关键操作重新鉴权重新登录后再测试或尝试维持会话 Cookie 不掉线优惠券重复使用报错服务端有幂等校验用两个不同账号同时并发提交同一张券订单号遍历返回空订单号是 UUID 或做了随机化放弃遍历改测订单号生成接口是否能被预测每一条经验都来自真实测试中的“先误报后实锤”或“先漏报后复盘”。不要小看表格里的验证方法它们能帮你少走很多弯路。7.2 我踩过的几个坑第一个坑没走完正常流程就开始改包。有一回我直接拿一个修改密码的请求改参数怎么改都没成功。后来老老实实走了一遍正常找回密码流程才发现真正的密码重置请求里有几个隐藏字段必须在会话里先拿到才能生效。不熟悉正常流程就不可能构造出有效的异常流程。第二个坑只关注请求参数忽略了业务上下文。比如购物车接口里某个商品 ID 看起来可以越权但要越权成功必须保证购物车里已经有对应商品。缺少前置业务上下文修改请求只能得到一个报错。第三个坑在正式环境里做测试却没有清理数据。逻辑漏洞测试经常会生成大量脏订单、脏优惠券记录。如果测试环境里有真实用户数据越权测试还可能读到别人的敏感信息。负责任的做法是使用专用的测试账号和虚拟数据测完立即清理。第四个坑把业务逻辑漏洞和普通越权混为一谈。越权可能只是访问控制配置错误业务逻辑漏洞往往是“规则设计本身有缺陷”。报告时要把二者分开描述否则开发人员会只修权限判断不修流程设计漏洞修不干净。7.3 漏洞报告与修复建议给开发团队提交业务逻辑漏洞报告时我一般会包含四部分内容正常业务流程图标注预期行为。异常请求与响应报文标注被篡改的参数。实际产生的业务影响比如“金额可被改为 0.01 元”或“任意用户密码可被重置”。修复建议具体到接口层和数据库层。修复建议要尽量可落地不要写空话。比如对价格篡改可以直接建议“在 checkout 接口中按商品 ID 重新从数据库查询单价并忽略前端传入的 total”。对流程跳过建议“在服务端会话中记录 emailVerified 状态未验证则禁止访问 reset 接口”。对双重用途接口建议“拆分为 /api/admin/user/update 和 /api/user/update 两个独立端点”。7.4 最后再分享一点个人经验业务逻辑漏洞测试本质上是在和开发者的思维盲区对抗。我发现 99% 的业务逻辑漏洞都不是靠某个神奇工具挖出来的而是靠“先完整走一遍正常流程再一个参数一个参数地改变状态”这种方式摸出来的。用 Burp Suite 其实只需要三个功能Proxy 抓包、Repeater 改包、 HTTP 历史排查。工具越简单思路反而越清晰。我现在的测试习惯是每测完一个功能点都会在本地留下一份“正常请求与异常请求对照表”。下次再遇到类似功能直接对照表里的位置去试效率比从头摸索高很多。这个方法同样适合刚入门的朋友不需要一开始就追求复杂工具链把一种业务场景测透比泛泛地刷十种靶场有用得多。