漏洞思维(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 / 安全测试中的权限分析方法。实际验证应优先使用自己控制的测试账号、测试对象和最小影响方式;理解对象关系和权限边界并不等于获得测试授权。

posted @ 2026-09-08 19:58  0xMouise  阅读(8)  评论(0)    收藏  举报