漏洞思维(4):权限不是改 ID——服务器凭什么允许你操作这个对象
漏洞思维(4):权限不是改 ID——服务器凭什么允许你操作这个对象
前言
上一篇,我已经开始把漏洞测试理解成:
正常允许组合
↓
改变一个业务变量
↓
构造禁止组合
↓
看服务器是否仍然允许
于是:
操作者
对象
动作
关系
状态
顺序
信任
都可以成为我主动改变的变量。
但继续往下以后,我发现其中有一类问题特别值得单独拆开。
就是:
权限
因为以前我一想到权限漏洞,脑子里很容易自动出现:
改 ID
换账号
低权限调用管理员接口
不登录直接访问
这些测试动作当然有用。
但它们都没有真正回答:
权限到底是什么?
如果这个问题不想明白,我还是会停在:
看到什么参数
就试什么参数
而不是:
先理解服务器为什么应该允许
再故意破坏允许条件
所以这一篇我只追一个问题:
服务器到底凭什么认为“这个人有权对这个对象做这件事”?
第一部分:先把“认证”和“授权”分开
一、认证回答“你是谁”,授权回答“你能不能做”
权限问题最容易混淆的一件事,就是:
身份认证
和:
权限授权
不是同一件事。
身份认证
身份认证回答:
你是谁?
例如通过:
用户名 + 密码
短信验证码
会话
JWT(身份令牌)
SSO(单点登录)
最后服务器得到:
当前用户 = 学生 A
到这里,只解决:
身份真实性
权限授权
身份确认以后,服务器还要继续回答:
学生 A
能不能
查看
请假申请 123
这才是:
授权
所以:
已经登录
≠
有权访问所有对象
甚至:
身份完全真实
≠
授权决策一定正确
一个系统完全可能:
认证正确
↓
用户身份也正确
↓
授权条件不完整
↓
仍然越权
所以以后我看到:
这个接口要求登录
我不会直接理解成:
这个接口已经安全
因为登录只告诉服务器:
你是谁
没有告诉它:
你到底能不能碰这个对象
第二部分:权限不是用户身上的一个开关
二、权限不是用户身上的一个开关
以前我会下意识觉得:
用户有权限
或者:
用户没权限
但继续拆以后,我发现这个说法太粗了。
权限不是单独存在于:
用户
也不是单独存在于:
接口
而是存在于:
某个主体,以某种动作,操作某个对象时的关系。
最基础可以写成:
操作者
+
动作
+
对象
例如:
学生 A
+
查看
+
成绩 A
=
允许
但:
学生 A
+
查看
+
成绩 B
=
拒绝
所以权限不是:
学生能不能“看成绩”
这么简单。
真正的问题是:
这个学生,能不能看这一份具体成绩?
三、ID 只是对象定位方式,不是权限本身
这也让我重新理解所谓:
改 ID
以前看到:
?id=1001
我会想:
改成 1002
但现在我知道:
1001
真正的意义不是:
一个可以修改的数字
而是:
一个对象定位符
它可能指向:
订单
文件
用户
申请
成绩
附件
定位符也不一定是数字。
它还可能是:
UUID
订单号
用户名
邮箱
文件名
路径
短链接令牌
随机字符串
所以:
难猜不等于有授权。
一个 UUID 很难枚举,
只能说明:
对象不容易被猜到
不能说明:
拿到对象定位符的人
就有权访问它
所以“改 ID”真正想验证的,其实是:
服务器在定位对象以后,有没有重新判断当前主体和这个对象之间的授权关系。
第三部分:水平和垂直越权,只是授权失败的两种表象
四、水平越权:同级主体之间的对象隔离失败
所谓水平越权,通常表现为:
用户 A
↓
访问
用户 B 的资源
两个用户可能拥有完全相同的角色:
学生 A
学生 B
角色都是:
学生
如果服务器只检查:
角色 == 学生
那么 A 和 B 之间并没有真正建立对象隔离。
真实业务往往还要求:
角色 == 学生
并且
目标对象所有者 == 当前学生
所以:
角色一样
≠
对象范围一样
水平越权真正破坏的是:
同级主体之间的对象隔离边界。
五、垂直越权:主体获得了本不属于自己的动作能力
垂直越权常见表现是:
普通用户
↓
执行
高权限角色动作
例如:
学生
+
审核通过
+
请假申请
=
拒绝
而:
辅导员
+
审核通过
+
负责范围内学生的请假申请
=
可能允许
所以垂直越权真正问的是:
当前动作,是否真的属于这个主体拥有的能力集合?
真正失败的不是:
“管理员页面被打开了”
而是:
本不属于当前主体的动作能力
被服务器错误授予
到这里先把水平和垂直理解成:
水平
→ 对象隔离失败
垂直
→ 动作能力边界失败
后面再用统一授权模型把它们放回同一个框架。
第四部分:真正复杂的权限,在“关系”和“范围”里
六、“你是不是老师”和“你是不是这个学生的老师”完全不同
这是我觉得权限思维里最重要的一层。
假设:
教师 A
确实拥有:
修改成绩
这个能力。
如果服务器只判断:
角色 = 教师
那可能得到:
教师 A
可以修改
所有课程成绩
但真实业务往往不是这样。
真正的规则可能是:
当前角色 = 教师
并且
目标课程的授课教师 = 当前教师
所以:
角色正确
并不代表:
关系正确
同样:
辅导员
有审核请假的能力,
但不代表:
所有学生的请假
都属于他的管理范围。
于是权限判断开始从:
角色
升级成:
角色
+
对象关系
七、我现在会问“为什么这个角色有权操作这个对象”
业务里常见关系包括:
所有者
创建者
成员
负责人
教师
学生
审核人
管理员
所属部门
所属店铺
所属租户
父对象 / 子对象
例如:
辅导员 X
↓ 负责
班级 1
↓ 包含
学生 A
↓ 拥有
请假申请 A
所以:
辅导员 X
有权审核:
请假申请 A
并不是因为:
他是辅导员
这四个字本身。
而是因为:
辅导员 X
→ 负责班级 1
→ 班级 1 包含学生 A
→ 学生 A 拥有请假申请 A
中间存在一条合法关系路径。
我现在更愿意把它叫:
授权关系路径
如果目标换成:
学生 B 的请假申请
而:
学生 B
不属于
辅导员 X 的负责范围
那么:
授权关系路径断了
理论结果应该:
拒绝
所以复杂权限真正验证的是:
当前主体到目标对象之间,是否存在业务允许的授权关系路径。
八、权限范围是权限模型里非常容易被忽略的东西
例如:
辅导员
确实拥有:
查看学生信息
但范围可能只是:
自己负责的学生
部门管理员可以:
查看员工
但范围可能是:
本部门
商家可以:
查看订单
但范围可能是:
本店铺
租户管理员可以:
管理用户
但范围可能是:
本租户
所以权限不能写成:
可以查看用户 = 是
更准确应该是:
可以查看用户
并且
目标用户 ∈ 当前主体允许查看的范围
于是:
检查了能力,却没有检查范围。
本身就是一类非常典型的权限缺口。
九、部门、班级、店铺、租户,本质上都可以理解成权限范围
虽然业务名字不同,
但从授权角度可以统一。
学校:
学院
↓
专业
↓
班级
↓
学生
企业:
公司
↓
部门
↓
团队
↓
员工
电商:
平台
↓
商家
↓
店铺
↓
订单
SaaS:
平台
↓
租户
↓
工作空间
↓
项目
↓
用户
它们共同在回答:
对象属于哪个范围?当前主体被允许操作哪个范围?
所以多租户系统里的:
tenantId
organizationId
workspaceId
不应该只被看成:
又一个 ID
而应该理解成:
权限范围边界的候选标识。
十、多租户隔离其实就是一个非常强的业务不变量
例如:
租户 A 的数据
绝不能被
租户 B 访问
这本身就是业务不变量。
于是授权判断中必须存在类似:
当前主体所属租户
==
目标对象所属租户
或者其他合法跨租户授权关系。
所以看到:
tenantId
我真正想问的不是:
能不能改
而是:
服务器最终使用哪个事实来确定当前租户和对象租户?
如果租户边界依赖客户端自己如实提交:
tenantId
那就已经值得继续追。
但这里先不展开“信任”本身。
因为后面会单独讲:
服务器为什么相信一个事实
第五部分:对象之间也会继承授权边界
十一、父对象和子对象不能被当成两个完全无关的世界
例如:
请假申请
↓
附件
如果:
学生 A
不能访问
学生 B 的请假申请
那么正常业务通常也要求:
学生 A
不能直接访问
该申请下的附件
因为附件并不是突然变成了一个公开资源。
它仍然属于:
请假申请
所以这里可以写出一个很重要的授权不变量:
子对象的访问权限不能因为换了入口,就弱于它所属父对象的权限边界。
类似关系还有:
订单
↓
发票
工单
↓
附件
项目
↓
文档
会话
↓
消息
这一篇只先建立:
父对象
↓
子对象
↓
授权关系应该正确传递
这个概念。
至于为什么不同接口、文件服务、导出入口会把同一条规则实现得不一致,
后面的“规则断层”再单独讲。
第六部分:动作本身也必须单独授权
十二、“能看”从来不等于“能改”
很多业务会模糊地说:
这个用户可以访问订单
但真正进入授权世界以后,
“访问”这个词太粗。
至少应该继续拆成:
查看
修改
删除
导出
退款
审核
下载
例如:
客服
→ 可以查看订单
并不意味着:
客服
→ 可以退款订单
学生:
可以查看成绩
并不意味着:
可以修改成绩
所以最好画成动作矩阵:
| 角色 | 查看 | 修改 | 删除 | 导出 | 审核 |
|---|---|---|---|---|---|
| 学生 | 自己范围 | 条件允许 | 条件允许 | 自己范围 | 拒绝 |
| 辅导员 | 负责范围 | 有条件 | 拒绝 | 负责范围 | 负责范围 |
| 管理员 | 全部 | 全部 | 全部 | 全部 | 全部 |
我真正优先关注的是:
拒绝
以及:
有条件允许
这些格子。
因为:
授权漏洞往往就出现在“本应拒绝”或者“条件不满足时本应拒绝”的地方。
第七部分:完整授权不是一个角色判断,而是一组条件
十三、真实授权更像 A ∧ B ∧ C ∧ D
真实业务很少是:
角色 = 教师
→ 永远允许修改成绩
更常见的是:
当前用户已经登录
并且
角色 = 教师
并且
目标课程属于当前教师
并且
目标学生属于这门课程
并且
当前动作 = 修改成绩
如果还有范围、状态、时间窗口等业务条件,
授权条件还会继续增加。
所以授权更像:
允许
如果
A ∧ B ∧ C ∧ D ...
这时候真正值得追的,不是:
有没有一个 authCheck()
而是:
完整授权条件集合里,有没有哪一个必要条件没有被真正保证?
例如业务要求:
C1:已登录
C2:角色 = 教师
C3:目标课程属于当前教师
C4:目标对象在当前教师权限范围内
程序却只检查:
C1 ∧ C2
那 C3、C4 就没有进入真正的授权决策。
所以很多权限问题并不是:
完全没有鉴权
而是:
授权逻辑只实现了业务规则的一部分
十四、所有权限漏洞最终可以继续追到三类根因
表面上我们会看到:
水平越权
垂直越权
未授权访问
对象越权
租户越权
部门越权
子对象越权
功能级越权
这些是不同的表现形式。
但继续往下追,根因通常可以统一成三类。
第一类:根本没有授权决策
检查已登录
↓
加载对象
↓
直接返回
服务器知道:
你是谁
却没有继续判断:
你能不能操作这个对象
第二类:做了授权,但条件不完整
业务真正要求:
角色
+
对象所有权
+
权限范围
+
具体关系
程序却只判断:
角色
这里不是“完全没鉴权”,
而是:
授权条件集合不完整
第三类:条件写了,但判断依据本身不可靠
例如代码确实判断:
当前用户 == 对象 owner
但 owner 不是由服务器保存的对象关系得到,
而是直接相信客户端提交。
那么表面上:
有判断
实际却是:
判断依据本身可以被用户影响
所以正确授权不仅需要:
条件存在
还需要:
参与授权的事实可信
“事实为什么值得相信”会在后面的信任边界篇继续深入。
十五、把各种“越权”重新放回同一个授权模型
现在再看:
水平越权
→ 对象所有权 / 同级主体隔离失效
垂直越权
→ 角色 / 动作能力边界失效
租户越权
→ 权限范围失效
部门越权
→ 组织范围失效
子对象越权
→ 父子对象授权关系失效
它们不再是完全不同的漏洞。
底层都在问:
哪一条授权条件,本来应该让这次操作变成拒绝,却没有真正进入服务器的授权决策?
所以权限漏洞可以统一成一句话:
一个本来应该被拒绝的授权组合,被服务器错误地判断成了允许。
第八部分:从一个数据包还原完整授权决策
十六、数据包只告诉服务器“你想做什么”,授权事实还要继续补齐
假设教师 A 正常修改自己课程里的一条成绩:
POST /api/score/update HTTP/1.1
Host: school.example.com
Cookie: SESSION=teacher_a
Content-Type: application/json
{
"scoreId": 9001,
"score": 85
}
如果只看数据包,
我能直接看到:
SESSION=teacher_a
→ 当前登录身份线索
scoreId=9001
→ 目标成绩对象
score=85
→ 用户希望执行的业务修改
但真正的授权决策还缺很多事实。
服务器至少还要继续还原:
SESSION
↓
当前主体 = 教师 A
scoreId=9001
↓
找到成绩对象 X
成绩 X
↓
属于课程 C
课程 C
↓
授课教师是谁?
当前动作
↓
修改成绩
教师 A
↓
是否拥有“修改成绩”能力?
课程 C
↓
是否属于教师 A 的负责范围?
只有这些条件都成立,
服务器才能得到:
允许
所以真正的授权判断不是:
请求里有 scoreId
也不是:
当前用户 role = teacher
而是:
主体
+
动作
+
对象
+
关系
+
范围
+
可信事实
=
授权决策
这也解释了为什么:
一个接口可以完全登录正常、角色也正确,但依然发生越权。
因为真正缺失的可能是:
对象关系
范围
或事实来源
而不是认证本身。
第九部分:完整走一遍——学校成绩系统
十七、先写出业务授权规则
假设系统有:
角色:
学生
教师
教务管理员
核心对象:
成绩
课程
选课关系
动作:
查看成绩
修改成绩
导出成绩
锁定成绩
对象关系:
学生选修课程
教师负责课程
成绩属于
“某个学生 × 某门课程”
的选课关系
业务规则:
R1:
学生只能查看自己的成绩
R2:
教师只能查看自己课程里的成绩
R3:
教师只能修改自己负责课程的成绩
R4:
普通教师不能锁定最终成绩
R5:
导出范围不能超过当前操作者的管理范围
十八、把“教师修改成绩”写成完整授权条件
人的业务规则:
教师只能修改自己负责课程里的成绩
进入授权模型以后:
当前主体 = 教师 A
动作 = 修改
目标对象 = 成绩 X
关系 =
成绩 X 属于课程 C
并且
课程 C 的授课教师 = 教师 A
权限范围 =
目标成绩属于教师 A 的课程范围
正常情况下:
教师 A
→ 修改
→ 自己课程成绩
=
允许
如果换成其他组合,
Expected 应该跟着变化:
| 授权组合 | 关键变化 | Expected |
|---|---|---|
| 学生 → 修改成绩 | 角色 / 动作能力不成立 | 拒绝 |
| 教师 A → 修改教师 B 课程成绩 | 授权关系不成立 | 拒绝 |
| 教师 → 锁定最终成绩 | 动作能力不属于普通教师 | 拒绝 |
| 教师 A → 导出全部课程成绩 | 超出权限范围 | 只允许自身范围 / 拒绝越界部分 |
这里不再重新教学第三篇的“怎么生成假设”。
这里只看一件事:
权限这个变量内部,到底是哪一个授权条件发生了变化。
这样一来:
换角色
换对象
断开关系
超出范围
换动作
就不再是五种孤立技巧,
而是五种不同的:
授权条件失效
第十部分:权限验证只沿用第三篇的控制变量方法
十九、两个自己控制的账号,是最干净的授权对照
第三篇已经讲过:
控制组
+
实验组
+
一次尽量只改变一个业务变量
权限测试里最干净的形式通常是:
测试账号 A
测试账号 B
资源 A
资源 B
正常基线:
A → A = 允许
B → B = 允许
再比较:
A → B = ?
这样我能确认:
资源确实存在
B 正常拥有它
A 也拥有同类资源
真正变化的是授权关系
所以在授权 SRC 环境里,
优先使用:
自己控制的测试账号
+
自己控制的测试对象
既更容易形成干净证据,
也能避免为了证明边界问题去接触不必要的真实用户数据。
第十一部分:权限接口七问
二十、看到敏感业务动作时,我快速问这七个问题
看到一个值得分析的业务动作,我会快速问:
1. 当前主体是谁?
身份到底怎么确定?
2. 目标业务对象是什么?
我现在到底在操作什么?
3. 当前动作是什么?
查看、修改、删除、审核、导出?
4. 主体和对象之间需要什么关系?
所有者、教师、负责人、成员?
5. 权限范围是什么?
自己、班级、部门、店铺、租户?
6. 还有没有额外授权条件?
这里先记录,不在本篇展开状态机细节。
7. 这些授权判断使用的事实来自哪里?
是服务端事实,还是客户端可以影响?
这七个问题的目标不是:
增加检查表长度
而是让我能够写出:
完整授权规则
因为只有先写出:
谁
在什么范围
通过什么关系
能对什么对象
执行什么动作
我才知道:
什么叫越界
第十二部分:这一篇的边界
二十一、不要把权限篇写成整个漏洞体系
权限继续往下,会自然碰到:
状态
不同入口
微服务
缓存
时间
信任
例如:
已锁定以后还能不能修改?
→ 状态问题
详情接口做了授权,导出接口没做?
→ 规则断层
权限上下文跨服务以后丢了?
→ 架构问题
owner 来自客户端?
→ 信任边界
这些都和权限有关,
但这一篇不继续展开。
因为第四篇真正要建立的只有一个核心:
授权不是“你有没有一个角色”,而是“当前主体是否通过合法关系,在正确范围内,对当前对象拥有这个动作能力”。
放回前四篇:
漏洞思维(1)
WHY
漏洞为什么会产生?
↓
漏洞思维(2)
WHAT
代码为了维持规则到底要判断什么?
↓
漏洞思维(3)
HOW
怎么从判断条件主动生成漏洞假设?
↓
漏洞思维(4)
专项放大:
服务器到底凭什么认为
当前主体有权操作当前对象?
到这里就够了。
结语
如果一定要把权限漏洞压成一句公式,我现在会写:
权限漏洞
=
一个本应被拒绝的
“主体 × 动作 × 对象 × 授权关系 × 权限范围”
组合
被服务器错误判断成允许
继续往下追,
错误通常来自三类:
没有授权决策
或
授权条件不完整
或
授权判断依赖了错误 / 不可信的事实
所以权限漏洞真正发生的地方,不是:
ID 被改了
而是:
业务定义的授权边界,和代码真正执行的授权边界,不一致。
我现在认为,
这才是理解“越权”最值得记住的一句话。
附:权限漏洞思维 ASCII 流程
┌───────────────────────────────┐
│ 业务权限规则 │
│ │
│ 谁,在什么范围,通过什么关系,│
│ 能对什么对象,执行什么动作? │
└───────────────┬───────────────┘
│
▼
┌───────────────────────────────┐
│ 当前主体是谁? │
│ 认证只解决身份 │
└───────────────┬───────────────┘
│
▼
┌───────────────────────────────┐
│ 写出授权模型 │
│ │
│ 主体 │
│ ↓ │
│ 角色 / 能力 │
│ ↓ │
│ 目标对象 │
│ ↓ │
│ 对象关系 │
│ ↓ │
│ 权限范围 │
│ ↓ │
│ 当前动作 │
│ ↓ │
│ 参与判断的事实是否可信? │
└───────────────┬───────────────┘
│
▼
┌───────────────────────────────┐
│ 理论结果:允许 / 拒绝 │
└───────────────┬───────────────┘
│
▼
┌───────────────────────────────┐
│ 最小化验证授权边界 │
└───────────────┬───────────────┘
│
┌───────┴────────┐
│ │
▼ ▼
┌──────────────┐ ┌────────────────┐
│ 实际被拒绝 │ │ 实际被允许 │
│ 授权边界成立 │ │ 授权边界被突破 │
└──────────────┘ └───────┬────────┘
│
▼
┌─────────────────┐
│ 继续追根因 │
│ │
│ 没有授权? │
│ 条件漏了? │
│ 事实不可信? │
└────────┬────────┘
│
▼
┌─────────────────┐
│ 权限漏洞 │
└─────────────────┘
注:本文讨论的是授权 SRC / 安全测试中的权限分析方法。实际验证应优先使用自己控制的测试账号、测试对象和最小影响方式;理解对象关系和权限边界并不等于获得测试授权。

浙公网安备 33010602011771号