漏洞思维(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 测试必须始终限制在明确授权的资产、账号、对象和测试规则范围内。

浙公网安备 33010602011771号