漏洞思维(2):把业务规则拆成代码能判断的条件

前言

上一篇,我一路从“人的业务世界”追到了“代码的业务世界”。

我最后慢慢想明白了一件事:

人的业务世界
↓
提出业务规则
↓
提炼业务不变量
↓
================
代码的业务世界
↓
程序具体实现
↓
================
再用业务不变量检查
代码有没有真正守住规则

也就是说:

业务不变量,是衡量代码有没有正确实现业务规则的一把安全尺子。

但到这里,我又遇到了一个新的问题。

业务人员说:

学生只能查看自己的成绩

在人脑里,这句话已经非常完整。

可代码根本不能直接执行:

“只能查看自己的成绩”

开发必须把这句话继续拆开。

代码至少得知道:

当前是谁?

他想查看什么?

这个东西属于谁?

他和这个东西是什么关系?

这个东西现在是什么状态?

前面应该发生的步骤发生了吗?

这些事实,服务器凭什么相信?

于是我突然意识到:

所谓业务建模,本质上就是把人的业务规则,拆成程序能够逐项判断的条件。

这一篇,我不想先学某一种漏洞。

我只想把这件事情彻底想清楚:

一条人脑里的业务规则,进入代码世界以后,到底会被拆成什么?


第一部分:为什么“看见接口”不等于“看懂业务”

一、接口只是代码入口,真正执行的是业务判断

刚开始抓包的时候,我特别容易被接口牵着走。

Burp 里可能出现:

/api/user/info
/api/order/detail
/api/order/update
/api/file/download
/api/leave/submit
/api/leave/review
/api/coupon/receive

以前我的第一反应是:

这里有 id
→ 改一下

这里有 status
→ 改一下

这里有 userId
→ 改一下

这个接口能重复
→ 多发几次

有时候确实能碰到漏洞。

但我后来发现,这种方式最大的问题不是“没效果”。

而是:

我不知道自己到底在验证哪一条业务规则。

例如,我在 Burp 里看到这样一组正常请求和响应:

从数据包翻译成业务语言(1):一个详情请求到底表达了什么?

GET /api/leave/detail?id=123 HTTP/1.1
Host: school.example.com
Cookie: SESSION=student_session
Accept: application/json

服务器返回:

HTTP/1.1 200 OK
Content-Type: application/json

{
  "id": 123,
  "studentId": 20260001,
  "reason": "Sick leave",
  "status": "submitted"
}

如果我只盯着技术表面,

我看到的是:

GET
/api/leave/detail
id=123
SESSION=student_session

但如果把它翻译成业务语言:

SESSION=student_session
↓
服务器识别当前操作者是谁

GET /api/leave/detail
↓
动作 = 查看

id=123
↓
目标业务对象 = 请假申请 123

studentId=20260001
↓
这个对象属于谁

status=submitted
↓
对象现在处于什么状态

于是这条请求就不再只是:

一个 HTTP 数据包

而变成:

当前操作者
+
查看
+
请假申请 123
+
对象归属
+
当前状态

也就是一次:

业务决策

这里有一个很重要的变化:

数据包里看到的是参数、Cookie 和 JSON;服务器真正要处理的是角色、动作、对象、关系和状态。

所以我现在更愿意认为:

接口只是让业务动作进入程序的入口,真正值得分析的是服务器为了决定“允许还是拒绝”,到底检查了哪些条件。


二、我需要的不是接口列表,而是一句“业务决策句子”

后来我给自己设计了一种非常简单的表达方式。

看到一个业务动作时,我先尝试把它写成:

谁
↓
对什么对象
↓
做什么
↓
他和对象是什么关系
↓
对象现在是什么状态
↓
前面必须发生什么
↓
服务器根据什么事实做判断

例如:

学生 A
↓
修改
↓
请假申请 123
↓
学生 A 是申请所有者
↓
申请状态 = 草稿
↓
申请尚未提交
↓
身份、所有者、状态都由服务端确认

这句话描述的是:

一条正常允许的业务组合

这句话已经足够重要。

因为它描述的不是:

一个参数
一个接口
一个请求包

而是:

为什么这一笔业务动作,在正常世界里应该被允许。

所以这一篇真正要建立的,不是一堆英文名词,而是一套:

把人的业务规则拆成程序可判断条件的方法。

第二部分:一条业务规则进入代码以后,会被拆成哪些条件

我现在先不追求复杂。

先拿一句最常见的业务规则:

辅导员只能审核自己负责学生已经提交的请假申请。

在人脑里,这是一句话。

但程序至少要拆成:

谁?
→ 辅导员

做什么?
→ 审核

审核什么?
→ 请假申请

为什么有权审核?
→ 他负责这个学生

什么时候能审核?
→ 申请已经提交

前面必须发生什么?
→ 学生完成提交

服务器凭什么知道这些是真的?
→ 身份、负责关系、状态都必须有可信来源

于是我开始得到几个固定维度。


三、第一个维度:角色——谁在操作?

第一个问题最简单:

谁在做这件事?

例如学校系统:

游客
学生
教师
辅导员
教务员
管理员

商城:

游客
普通用户
会员
商家
客服
财务
审核员
平台管理员

角色为什么重要?

因为业务里天然存在:

不同的人
拥有不同的能力

例如:

学生
→ 查看自己的成绩

教师
→ 查看自己负责课程的成绩

管理员
→ 查看更大范围的数据

所以角色真正帮我看到的是:

身份边界

例如:

游客 → 登录用户
普通用户 → 管理员
学生 → 教师
教师 A → 教师 B 的负责范围

以后我看到角色时,不再只是统计:

系统有几个账号

而会问:

这里有哪些身份边界,本来绝对不能被跨过去?


四、第二个维度:业务对象——大家到底在围绕什么东西办事?

有了“谁”,下一步就是:

他正在操作什么东西?

以前我习惯叫它:

资源

但现在我更喜欢叫:

业务对象

因为这样更容易和上一篇“核心业务对象”的思路接起来。

例如学校系统:

学生档案
成绩
课程
请假申请
附件
审核记录
通知
工单

商城:

订单
购物车
优惠券
地址
余额
退款单
发票

以前我看到:

id=123

会直接想:

改成 124

现在我先问:

123 到底代表什么业务对象?

如果:

orderId=123

那我还要继续知道:

订单 123 属于谁?

例如:

123 → 用户 A
124 → 用户 B

于是:

A 请求 124

真正表达的就不是:

“改了一个 ID”

而是:

当前操作者 = A
目标对象 = B 的订单

所以找到业务对象以后,我才真正知道:

当前这次操作,碰到的是谁的什么东西。


五、第三个维度:动作——他想对这个对象做什么?

只有:

人
+
对象

还不够。

因为同一个人,对同一个对象,也可能:

能看
但不能改

能创建
但不能审核

能下载
但不能删除

所以第三个问题是:

他想做什么?

常见动作可以理解成:

创建
查看
修改
删除
提交
下载
上传
审核
通过
拒绝
支付
退款
领取
使用
导出
绑定
解绑

于是最基础的业务决策骨架出现了:

谁
+
对什么对象
+
做什么

例如:

学生
+
查看
+
自己的成绩

正常应该允许。

而:

学生
+
修改
+
自己的成绩

理论上应该拒绝。

再例如:

普通用户
+
审核
+
申请

也应该拒绝。

这时候我真正得到的不是一个漏洞结论,

而是:

同一个角色
面对同一个对象
不同动作
可能拥有完全不同的业务权限

所以:

角色
+
对象

还不能描述完整业务规则。

必须再加:

动作

程序才能知道:

当前这个人,对当前这个对象,究竟想做什么。


六、前三个维度组合起来,是最基础的业务决策骨架

现在我已经有:

角色
+
动作
+
对象

例如请假系统:

角色 查看自己的申请 查看别人申请 修改自己的申请 审核申请
学生 允许 拒绝 条件允许 拒绝
辅导员 允许 负责范围内允许 条件允许 负责范围内允许
管理员 允许 允许 允许 允许

这张表最重要的意义不是:

马上去验证哪些拒绝组合

而是让我看见:

同一个系统里,“谁 + 对什么对象 + 做什么”本身就已经构成一层业务规则。

例如:

学生
+
查看
+
自己的申请

和:

学生
+
审核
+
申请

虽然角色和对象可能相同,

但因为:

动作不同

业务结果就完全不同。

不过到这里仍然不够。

因为现实业务往往还会继续问:

为什么这个角色有权操作这个具体对象?

现在这个对象处于什么阶段?

前面必须完成什么?

这些事实又是谁提供的?

所以接下来还要继续补:

关系
状态
顺序
信任

七、第四个维度:关系——为什么这个角色有权操作这个对象?

继续往下,我发现:

角色 + 动作 + 对象

仍然不够。

因为:

“你是不是老师”

和:

“你是不是这个学生的老师”

根本不是一回事。

例如:

辅导员 A
负责班级 1

学生 B
属于班级 2

辅导员 A 虽然确实是:

辅导员

也确实拥有:

审核请假

这个能力。

但不代表:

他可以审核所有学生的请假

真正的业务规则其实是:

角色 = 辅导员
+
当前学生 ∈ 该辅导员负责范围
+
动作 = 审核
+
目标对象 = 学生请假申请

所以权限开始从:

角色检查

进一步变成:

角色
+
对象关系

业务里常见关系很多:

所有者
创建者
成员
负责人
审核人
教师
学生
父对象
子对象
所属部门
所属租户

这时候我开始问:

服务器检查的是“你是什么角色”,还是“你和这个具体对象到底是什么关系”?

这里特别适合从一个正常审核请求来看。

从数据包翻译成业务语言(2):关系为什么不一定写在请求里?

POST /api/leave/review HTTP/1.1
Host: school.example.com
Cookie: SESSION=counselor_session
Content-Type: application/json

{
  "leaveId": 123,
  "result": "approved"
}

从数据包本身,我只能直接看到:

SESSION
→ 当前登录身份的线索

leaveId=123
→ 要审核哪一份请假申请

result=approved
→ 想执行什么审核结果

但真正决定这个辅导员有没有资格审核的关键事实:

“他是不是这个学生的辅导员?”

并没有直接写在请求里。

服务器必须自己继续还原:

SESSION
↓
当前辅导员 A

leaveId=123
↓
找到请假申请 123
↓
找到申请人学生 B

学生 B
↓
属于哪个班级 / 负责范围

辅导员 A
↓
是否负责学生 B

最后才能得到:

关系成立
或
关系不成立

这让我意识到:

有些最关键的业务条件根本不会直接出现在 HTTP 数据包里。请求只告诉服务器“我要操作谁”,真正的关系事实往往必须由服务器自己查询和确认。

这也是为什么只盯着参数名,很容易看漏真正的授权条件。


八、第五个维度:状态——什么时候才允许做?

前面的几个维度主要回答:

谁能对什么做什么?

但很多业务权限并不是永久不变。

同一个人、同一个对象、同一个动作:

在不同状态下
结果可能完全不同

例如请假申请:

草稿
↓
已提交
↓
审核中
↓
已通过 / 已拒绝

学生修改自己的请假:

草稿
→ 可以修改

但:

已通过
→ 通常不能继续按草稿规则修改

于是业务决策条件继续扩展:

谁
+
做什么
+
对什么对象
+
对象是什么状态

例如:

学生
+
修改
+
自己的请假
+
草稿
=
允许

而:

学生
+
修改
+
自己的请假
+
已通过
=
拒绝

所以状态真正回答的是:

这件事现在能不能做?


九、第六个维度:顺序和前置条件——这件事之前必须发生什么?

状态还不能覆盖全部流程问题。

例如密码重置:

输入账号
↓
验证码验证
↓
设置新密码

即使最终状态叫:

允许重置

我还是要问:

这个状态是怎么来的?

也就是:

前面必须发生什么?

例如:

只有验证码真正验证成功
↓
才能获得“允许重置密码”的资格

所以:

状态

描述的是:

现在在哪

而:

顺序 / 前置条件

描述的是:

你为什么有资格来到这里

这也是为什么:

页面上必须按 1 → 2 → 3

不等于:

服务器真的保证了 1 → 2 → 3

所以以后看到流程,我会继续问:

谁在服务器端保证这一步之前的条件真的发生过?

从数据包翻译成业务语言(3):两个接口之间,谁来证明“前一步真的完成了”?

例如正常的密码重置流程。

先验证验证码:

POST /api/password/verify-code HTTP/1.1
Host: account.example.com
Content-Type: application/json

{
  "account": "student@example.com",
  "code": "123456"
}

服务器确认验证成功以后,返回一个短期重置凭证:

HTTP/1.1 200 OK
Content-Type: application/json

{
  "resetToken": "reset_token_example"
}

随后用户才能提交新密码:

POST /api/password/reset HTTP/1.1
Host: account.example.com
Authorization: Reset reset_token_example
Content-Type: application/json

{
  "newPassword": "Example-New-Password"
}

从服务器视角看,真正重要的是:

verify-code
↓
验证码验证成功
↓
服务器产生“允许重置”的可信资格
↓
reset
↓
再次验证这份资格
↓
允许修改密码

所以:

状态
= 现在是否拥有“允许重置”的资格

顺序 / 前置条件
= 这份资格是不是由前一步真实、合法地产生

这比“页面必须按 1 → 2 → 3”更接近真正的业务规则。


十、第七个维度:信任来源——服务器凭什么知道这些条件是真的?

到这里,前面的条件其实都还是:

业务世界要求程序判断什么

现在终于进入最关键的一层:

程序拿什么事实来做这些判断?

例如服务器要判断:

当前是谁?
对象属于谁?
现在是什么状态?
价格是多少?
是否已经审核?
验证码是否验证?

这些都必须有来源。

我现在会把来源粗略分成两类。

客户端可以影响的输入

URL 参数
JSON
Cookie
Header
隐藏字段
前端计算结果
页面状态
localStorage

服务端更有资格作为权威事实的来源

服务端会话
数据库中的真实身份
数据库中的对象所有者
数据库中的业务状态
服务器重新计算的金额
服务端产生并验证的短期凭证

例如,我在一个正常的下单请求里看到:

从数据包翻译成业务语言(4):请求里“有这个值”不等于服务器“应该相信这个值”

POST /api/order/create HTTP/1.1
Host: shop.example.com
Cookie: SESSION=user_session
Content-Type: application/json

{
  "productId": 1008,
  "quantity": 2,
  "userId": 10001,
  "price": 99.00,
  "role": "user"
}

如果只从 HTTP 角度看,

这些都只是:

JSON 字段

但翻译成业务语言以后,它们的性质并不一样:

字段 它表达的业务含义 谁更有资格成为权威来源
productId 用户想购买哪个商品 客户端可以表达选择,服务端再确认对象
quantity 用户想购买多少 客户端可以表达意图,服务端负责校验限制
userId 当前是谁 通常应由服务端身份上下文确认
price 商品真实价格 商品数据 / 服务端计算
role 当前拥有什么权限 服务端认证与授权数据

所以我现在不会把:

请求里出现了 userId

直接等价成:

服务器就应该以这个 userId
作为当前身份

也不会把:

请求里出现了 price

直接等价成:

这个价格就是真实业务价格

真正的问题是:

这个字段是在表达“用户想做什么”,还是在宣布“业务事实是什么”?

如果它只是用户意图,

客户端当然可以提交。

如果它代表关键业务事实,

就要继续问:

谁才有资格证明这个事实成立?

所以:

数据包里出现一个值
≠
这个值拥有业务权威性

这一点,就是“信任来源”真正要解决的问题。

这就是上一篇讲的:

信任

在业务模型里的具体落点。


第三部分:把七个条件重新合在一起

十一、一条业务动作,可以被我写成一个完整判断模型

到这里,我已经得到七个固定问题:

角色
↓
动作
↓
业务对象
↓
关系
↓
状态
↓
顺序 / 前置条件
↓
信任来源

例如:

学生 A
↓
修改
↓
请假申请 123
↓
A 是该申请所有者
↓
状态 = 草稿
↓
申请尚未提交
↓
身份、所有者、状态都由服务端确认

这时候,我终于能够完整回答:

为什么这一次操作,在正常业务里应该被允许?

因为它不是只满足一个条件,

而是:

角色正确
+
动作允许
+
对象正确
+
关系成立
+
状态允许
+
前置条件成立
+
判断事实来自可信来源

这些条件共同组成:

一次完整的业务决策

所以所谓业务建模,

本质上就是:

把人脑里一句完整的业务规则,拆成程序真正需要逐项判断的条件。


十二、业务不变量告诉我“必须保证什么”,七个维度告诉我“代码必须判断什么”

例如业务不变量:

学生只能修改自己的草稿请假申请。

程序为了维持它,需要保证:

角色 = 学生
动作 = 修改
对象 = 请假申请
关系 = 当前学生是该申请所有者
状态 = 草稿
顺序 = 尚未进入不可修改阶段
身份 / 所有者 / 状态 = 来自权威来源

所以两边可以直接连起来:

业务不变量
↓
规定“什么必须始终成立”
↓
角色 / 对象 / 动作 / 关系 / 状态 / 顺序 / 信任
↓
告诉我“代码必须判断什么”
↓
程序实现
↓
最终业务决策

到这里,第二篇的抽象模型已经闭合。

下一部分不再继续解释概念,

而是拿一个完整业务把这套模型真正跑一遍。


第四部分:完整走一遍——学校请假系统

十三、先不要找漏洞,先把业务拆出来

假设我拿到一个学校请假系统。

我先不改 ID。

也不先想越权。

我只做建模。

角色

学生
辅导员
管理员

核心业务对象

请假申请
附件
审核记录
学生档案

其中最值得一路追的主对象是:

请假申请

因为:

学生创建它
学生修改它
学生提交它
辅导员审核它
它会不断改变状态
附件和审核记录也围绕它产生

动作

创建
查看
修改
删除
提交
审核
下载附件

关系

学生
↓ 拥有
请假申请

辅导员
↓ 负责
学生

请假申请
↓ 包含
附件

请假申请
↓ 产生
审核记录

状态

草稿
↓
已提交
↓
审核中
↓
已通过 / 已拒绝

顺序 / 前置条件

必须先创建
↓
才能提交

必须先提交
↓
才能审核

审核结束以后
↓
不能继续按照草稿规则修改

信任来源

当前身份
→ 服务端会话

请假所有者
→ 数据库

申请状态
→ 数据库

辅导员与学生关系
→ 数据库 / 服务端业务关系

是否已经提交
→ 服务端状态

十四、再写业务不变量

现在我才开始写:

学生只能读取自己的申请。

学生只能修改自己处于草稿状态的申请。

学生不能执行审核动作。

辅导员只能审核自己负责学生的申请。

没有提交的申请不能进入审核。

已经审核完成的申请不能重新按草稿规则修改。

附件的访问权限不能弱于所属请假申请。

这时候每一条业务不变量,都可以重新映射到模型:

例如:

学生只能修改自己处于草稿状态的申请

对应:

角色
→ 学生

动作
→ 修改

对象
→ 请假申请

关系
→ 当前学生 = 申请所有者

状态
→ 草稿

信任
→ owner / state 均由服务端确认

这就是:

从人脑里的规则,翻译成代码必须维持的条件。


十五、到这里先停:这个例子的“正常世界”已经建完了

现在我已经得到:

角色
对象
动作
关系
状态
顺序
信任来源
业务不变量

而且已经知道:

一条人的业务规则
↓
如何拆成
代码必须判断的一组条件

例如:

学生只能修改自己的草稿申请

可以被翻译成:

学生
+
修改
+
请假申请
+
所有者关系成立
+
状态 = 草稿
+
前置条件成立
+
事实来源可信
=
正常允许

这就是这一篇需要做到的终点:

先把一个正常业务动作为什么应该被允许,完整地建模出来。

后面的漏洞假设,都应该建立在这个正常模型之上。


第五部分:这一篇在整个漏洞思维体系里的位置

十六、后面的专项文章,都在放大今天的某一个维度

现在我已经有:

角色
对象
动作
关系
状态
顺序
信任

后面的很多专项问题,其实都只是把其中一个维度继续放大:

权限
→ 重点追角色 / 动作 / 对象 / 关系 / 范围

状态机
→ 重点追状态 / 顺序 / 前置条件

信任边界
→ 重点追这些事实到底来自哪里

对象生命周期
→ 继续追这些规则能不能贯穿对象的一生

规则断层
→ 比较同一条规则在不同入口里是否一致

并发
→ 再把时间加入业务条件

所以第二篇不是在教某一种具体漏洞。

它真正做的是:

先建立一套看业务规则的坐标系。


十七、前三篇的分工:WHY → WHAT → HOW

漏洞思维(1) —— WHY
为什么会产生漏洞?

漏洞思维(2) —— WHAT
代码为了实现人的业务规则
到底必须判断什么?

漏洞思维(3) —— HOW
已经知道这些业务条件以后
怎么从正常允许世界
推导出理论上应该被拒绝的禁止世界?

所以这一篇最重要的终点不是:

我已经开始测试漏洞

而是:

我已经能够解释
一个正常业务动作
为什么应该被允许 / 拒绝

只有把正常世界建清楚,

下一篇改变条件时,

我才知道自己究竟改变了哪一条业务规则。


结语

这一篇真正让我建立起来的,不是一组英文名词。

而是一种新的看法:

HTTP 数据包
↓
不是业务本身
↓
只是把一部分输入送进服务器
↓
服务器还必须结合:
身份
对象关系
状态
前置条件
权威事实
↓
才能做出真正的业务判断

所以:

看懂数据包,不等于看懂业务;真正的业务分析,是把数据包重新翻译成人的规则,再看服务器到底依赖哪些条件去执行这些规则。

我现在越来越觉得:

真正理解业务漏洞之前,第一步不是学会怎么改参数,而是先知道代码为了维持这条业务规则,到底必须判断哪些条件。

这一篇到这里就够了。

下一篇再继续追:

如果我故意让其中一个条件不成立,会发生什么?


附:全文 ASCII 思维流程

┌────────────────────────────┐
│        人的业务世界        │
│                            │
│  业务目标 → 业务规则       │
└─────────────┬──────────────┘
              │
              ▼
┌────────────────────────────┐
│         业务不变量         │
│                            │
│ 哪些安全约束必须始终成立? │
└─────────────┬──────────────┘
              │
              │  作为安全标准
              ▼
══════════════════════════════
       进入代码的业务世界
══════════════════════════════
              │
              ▼
┌────────────────────────────┐
│        HTTP / API 输入      │
│                            │
│ 参数 / Cookie / JSON       │
│ Header / Token             │
└─────────────┬──────────────┘
              │
              │  翻译成业务语言
              ▼
┌────────────────────────────┐
│   把业务规则拆成判断条件   │
│                            │
│  角色:谁?                │
│  对象:操作什么?          │
│  动作:做什么?            │
│  关系:为什么有权?        │
│  状态:什么时候能做?      │
│  顺序:之前必须发生什么?  │
│  信任:凭什么认为是真的?  │
└─────────────┬──────────────┘
              │
              ▼
┌────────────────────────────┐
│          程序实现          │
│                            │
│ 获取服务端事实             │
│      +                     │
│ 接收客户端意图             │
│      ↓                     │
│ 条件判断 → 业务动作        │
└─────────────┬──────────────┘
              │
              ▼
┌────────────────────────────┐
│      完整业务决策模型      │
│                            │
│ 为什么这一次应该允许 / 拒绝│
└────────────────────────────┘

注:本文讨论的是授权范围内的漏洞分析与业务建模方法。理解业务关系不等于获得测试授权;实际 SRC 测试必须始终限制在明确授权的资产、账号、对象和测试规则范围内。

posted @ 2026-09-04 18:28  0xMouise  阅读(7)  评论(0)    收藏  举报