查询 25 次,耗时 0.6227 秒

从本地靶场验证到修复:系统化测试 Web API 对象级越权

lawrenceyang

New member

lawrenceyang Rep
0
0
0
Rep
0
lawrenceyang Vouches
Vouches
0
1 MONTH
1 1 MONTH OF SERVICE
lawrenceyang 获得
0
0
0
获得
0
lawrenceyang 凭证
0
0
0
凭证
0
主题
2
点赞
0
HakB
1 月
在社区成长 1 月
LEVEL 1 1 XP
 
## 背景

在 APP 和 Web 项目中,用户登录后通常会通过 API 查询订单、工单、文件或个人资料。一个常见的访问控制缺陷是:服务端确认了用户已经登录,却没有确认当前用户是否有权访问请求中的具体对象。攻击者如果在授权测试环境里把自己有权访问的对象标识换成另一条记录的标识,接口仍返回数据,就构成对象级授权缺失,常称为 IDOR 或 BOLA。它不是简单的参数校验问题,而是服务端对象访问策略没有落实。

本文用仅监听本机的 Flask 小靶场演示测试和修复。示例数据、令牌和编号均为虚构内容;不要把测试请求发往未获书面授权的系统。

## 原理与测试目标

登录认证回答的是“请求者是谁”,对象级授权回答的是“这个请求者能否对这条具体记录执行当前操作”。两者不能互相替代。对象编号是数据库主键、UUID 还是难以猜测的字符串,都不能代替授权判断;前端隐藏按钮也不能保护后端接口。

本次测试要验证三件事:有效用户能访问自己的记录;另一个有效用户不能读取或修改该记录;未认证请求会被拒绝。读取接口通过后,还应在靶场内检查更新、删除、下载等操作,因为不同接口可能采用不同的授权逻辑。

## 前置条件

准备一台本地开发环境和 Python 3,安装 Flask。测试只使用 `127.0.0.1` 上的服务,不连接公网目标。最好准备两个测试身份和各自拥有的一条虚构记录。若是在团队项目中测试共享环境,应先确认书面授权、测试范围、允许的账号、时段和数据处理方式。

创建 `app.py`,写入以下有意省略对象归属校验的示例:

```python
from flask import Flask, jsonify, request

app = Flask(__name__)
TOKENS = {'token-alice': 'alice', 'token-bob': 'bob'}
ORDERS = {
'A-1001': {'owner': 'alice', 'total': 42},
'B-2001': {'owner': 'bob', 'total': 88},
}

@app.get('/api/orders/<order_id>')
def get_order(order_id):
raw = request.headers.get('Authorization', '')
user = TOKENS.get(raw.removeprefix('Bearer '))
if not user:
return jsonify(error='unauthorized'), 401
row = ORDERS.get(order_id)
if row is None:
return jsonify(error='not found'), 404
# 靶场缺陷:已认证,但没有检查 row 的 owner
return jsonify(id=order_id, total=row['total'])

if __name__ == '__main__':
app.run(host='127.0.0.1', port=5000, debug=False)
```

这里的令牌仅为本地演示,不是生产级认证方案。启动服务:

```bash
python -m venv .venv
. .venv/bin/activate
python -m pip install Flask
python app.py
```

Windows 环境可使用虚拟环境对应的 `Scripts` 激活脚本。服务启动后保持终端运行,在另一个终端执行测试。

## 分步骤实践

### 1. 验证正常访问

以 Alice 身份读取 Alice 的记录:

```bash
curl -i -H 'Authorization: Bearer token-alice' http://127.0.0.1:5000/api/orders/A-1001
```

预期得到 200 响应和虚构订单金额。这一步建立正常功能基线,避免把接口本身不可用误判为安全问题。

### 2. 验证对象归属边界

仍然使用 Alice 的身份,请求 Bob 的记录:

```bash
curl -i -H 'Authorization: Bearer token-alice' http://127.0.0.1:5000/api/orders/B-2001
```

在有意设置缺陷的版本中,这个请求会返回 200。说明服务端只验证了身份,却未验证记录归属。再使用 Bob 读取 `B-2001`,应确认 Bob 自己的正常请求仍可成功。测试时要记录请求方法、路径、身份、预期状态码和实际结果,不要收集超出验证所需的数据。

### 3. 修复服务端授权

在 `get_order` 中,取得记录并确认它存在后、返回数据前加入对象归属检查:

```python
if row['owner'] != user:
return jsonify(error='not found'), 404
return jsonify(id=order_id, total=row['total'])
```

重新启动服务,再执行 Alice 读取 Bob 记录的请求,预期为 404;Alice 读取自己的记录仍为 200,未提供有效令牌的请求为 401。真实系统也可以使用 403 表示已认证但无权限。选择 403 还是 404 应符合产品的一致设计;404 有时能减少对象是否存在的信息泄露,但不能替代正确授权。

### 4. 扩展验证与回归

在授权靶场中,对每类对象和每种操作建立权限矩阵,例如“用户 A 读取 A 的记录”“用户 A 读取 B 的记录”“用户 A 更新 B 的记录”“未登录用户下载文件”。每条路径都检查服务端的最终响应和数据是否真的改变;仅看到前端报错并不足以证明后端拒绝了请求。

可将关键用例放进集成测试。例如使用 Flask 测试客户端验证跨用户读取返回拒绝,并同时断言 Alice 的自有记录仍可读取。对更新和删除接口,还要确认越权请求不会产生副作用。测试数据应隔离并在结束后清理,避免测试用例污染共享环境。

## 风险与注意事项

在生产或共享测试环境中,越权验证可能造成个人信息暴露、误更新或删除。只使用授权账号和明确列入范围的对象;优先在本地、预发布或专用测试租户验证;控制请求速率;不要批量枚举编号;发现疑似真实数据时立即停止读取并按约定报告。日志与报告中应遮盖令牌、个人信息和业务敏感字段。测试发现问题后,不要为了证明影响而扩大访问范围。

修复时不要只在客户端隐藏入口,也不要仅改用随机 ID。应在服务端围绕当前认证主体、对象及操作类型执行授权,并避免不同路由各自复制不一致的判断逻辑。对列表、导出、搜索、附件下载和批量操作同样需要检查权限;只保护详情接口容易留下旁路。

## 常见问题

**对象 ID 换成 UUID 后还需要授权吗?** 需要。UUID 只能降低猜测便利性,不能证明请求者有权访问对象。

**用户只能从自己的页面看到记录,是否已经安全?** 不一定。浏览器界面不是安全边界,直接调用 API 时,服务端仍必须独立检查权限。

**为什么对无权对象返回 404?** 这是一种减少对象存在性泄露的接口设计选择;也可以使用 403。关键是无权请求不能返回数据或产生副作用,并且行为在同类接口中保持一致。

**有多个组织或租户时怎么测?** 在授权环境中加入跨租户测试,确认查询条件和授权策略都绑定当前用户可访问的租户;不能只依赖客户端提交的租户编号。

## 总结

对象级越权测试的核心不是猜编号,而是用明确授权的测试身份验证服务端是否对每个具体对象、每种操作执行了权限判断。先建立正常访问基线,再验证跨用户边界,随后修复服务端授权并用自动化回归覆盖读取、修改、删除和下载等路径。配合最小化数据、限定范围和谨慎记录,才能让测试结果既可复现,也不会把验证过程变成新的风险。
 
顶部