查询 32 次,耗时 0.5275 秒

从接口越权到回归测试:在本地复现并修复 Web/API 对象级授权缺失(BOLA/IDOR)

dead3223

New member

dead3223 Rep
0
0
0
Rep
0
dead3223 Vouches
Vouches
0
1 MONTH
1 1 MONTH OF SERVICE
dead3223 获得
0
0
0
获得
0
dead3223 凭证
0
0
0
凭证
0
主题
11
点赞
0
HakB
1 月
在社区成长 1 月
LEVEL 1 1 XP
 
作者:dead3223
板块:APP/Web渗透

一、背景:为什么“已经登录”仍然可能读到别人的订单

在 APP 和 Web 项目中,订单详情、收货地址、附件下载、工单记录等功能,通常通过对象标识访问资源,例如 GET /api/orders/1001。客户端虽然只展示当前用户的订单,但攻击者并不需要遵守客户端的交互限制,可以直接构造 HTTP 请求。

如果服务端仅验证“请求来自某个已知用户”,没有继续验证“这个用户是否有权访问当前订单”,就会出现对象级授权缺失。此类问题常被称为 BOLA,涉及直接对象引用时也经常使用 IDOR 这一名称。

本文不涉及真实站点探测。所有账号、订单和接口均在本机创建,目标是建立一条可以复现、可以解释、可以自动回归的证据链:同一对象、两个身份、修复前后对照。

二、漏洞原理:身份认证与对象授权是两道不同的检查

身份认证回答“你是谁”,对象授权回答“你能否对这个对象执行当前操作”。

错误实现的逻辑通常是:
1. 从请求中得到用户身份。
2. 根据 URL 中的订单 ID 查询订单。
3. 订单存在就返回。

缺失的是第二步与第三步之间的权限判断,例如订单的 owner_id 是否等于当前用户 ID。

需要特别注意:整数 ID 只是让问题更容易暴露,并不是漏洞根因。即使把 ID 换成 UUID,只要标识通过日志、链接或其他接口泄露,缺失对象授权的问题仍然存在。

本文采用最简单的“用户只能读取自己的订单”规则。真实业务可能还包含租户、团队、角色、共享范围和订单状态,应把这些条件纳入统一授权策略。

三、前置条件与实验边界

准备 Python 3.10 或更新版本,以及一个独立实验目录。依赖仅包括 Flask 和 pytest。

执行命令:
python -m venv .venv

Linux/macOS 激活:
source .venv/bin/activate

Windows PowerShell 激活:
.venv\Scripts\Activate.ps1

安装依赖:
python -m pip install flask pytest

本文为了把注意力集中在对象授权,使用 X-Lab-User 请求头模拟“认证中间件已经确认的用户身份”。它不是安全的认证方案,不能直接用于线上系统。生产环境应从经过验证的会话或令牌中提取身份,而不是信任客户端自报的用户 ID。

实验服务仅监听 127.0.0.1,不接入公网,不使用真实个人信息,不把其他设备或真实业务系统作为验证对象。

四、步骤一:建立同时包含漏洞版与修复版的本地接口

创建 app.py:

from flask import Flask, abort, jsonify, request

app = Flask(__name__)

USERS = {1: 'alice', 2: 'bob'}
ORDERS = {
1001: {'id': 1001, 'owner_id': 1, 'item': '实验键盘'},
1002: {'id': 1002, 'owner_id': 2, 'item': '实验鼠标'},
}


def current_user_id():
raw = request.headers.get('X-Lab-User', '')
if raw not in {'1', '2'}:
abort(401)
return int(raw)


@app.get('/lab/vulnerable/orders/<int:eek:rder_id>')
def vulnerable_order(order_id):
current_user_id()
order = ORDERS.get(order_id)
if order is None:
abort(404)
return jsonify(order)


@app.get('/lab/fixed/orders/<int:eek:rder_id>')
def fixed_order(order_id):
user_id = current_user_id()
order = ORDERS.get(order_id)
if order is None or order['owner_id'] != user_id:
abort(404)
return jsonify(order)


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

启动服务:
python app.py

这里刻意保留两条不同路径,方便进行对照。漏洞版虽然调用了 current_user_id(),却没有使用返回的身份约束订单读取。修复版则将“订单属于当前用户”作为返回数据的必要条件。

五、步骤二:建立正常访问基线

另开终端,以 Alice 的身份读取 Alice 的订单:

curl -i -H "X-Lab-User: 1" http://127.0.0.1:5000/lab/vulnerable/orders/1001

应当得到 HTTP 200,JSON 中包含 id 为 1001、owner_id 为 1 的订单。

再验证无身份请求:

curl -i http://127.0.0.1:5000/lab/vulnerable/orders/1001

应当得到 HTTP 401。

这一步证明接口确实存在一层身份检查,但不能据此认定订单访问是安全的。测试报告中应分别记录认证检查和授权检查,避免把两者混为一谈。

六、步骤三:只改变对象标识,验证水平越权

保持 Alice 的身份不变,把目标订单换成明确属于 Bob 的 1002:

curl -i -H "X-Lab-User: 1" http://127.0.0.1:5000/lab/vulnerable/orders/1002

根据代码,此请求会得到 HTTP 200,且响应包含 owner_id 为 2 的订单。这就是本实验的越权证据:身份没有变化,访问对象从自己的资源切换成了他人的资源,服务端仍然返回内容。

这里不需要批量枚举,也不需要扫描 ID 范围。两个已知归属的测试对象已经足以说明授权条件缺失。授权测试应优先采用最小证据原则,减少对数据和服务的影响。

再进行反向验证:

curl -i -H "X-Lab-User: 2" http://127.0.0.1:5000/lab/vulnerable/orders/1001

这一请求同样会暴露另一个方向的跨用户读取,说明问题不是某个测试账号配置异常。

七、步骤四:验证修复结果与状态码语义

先确认修复没有破坏正常业务:

curl -i -H "X-Lab-User: 1" http://127.0.0.1:5000/lab/fixed/orders/1001

预期为 HTTP 200。

随后重复越权请求:

curl -i -H "X-Lab-User: 1" http://127.0.0.1:5000/lab/fixed/orders/1002

预期为 HTTP 404,不返回 Bob 的订单内容。

本实验对“对象不存在”和“对象存在但不属于当前用户”统一返回 404,目的是减少通过响应直接区分对象存在性的机会。业务也可以在明确需要的场景使用 403,但必须建立一致的语义。统一状态码不意味着所有旁路信息都被消除,响应时间、响应体、缓存和其他接口仍需独立检查。

八、步骤五:将修复固化为自动化测试

创建 test_authz.py:

import pytest
from app import app


@pytest.fixture
def client():
app.config['TESTING'] = True
with app.test_client() as client:
yield client


def test_lab_vulnerability_is_reproducible(client):
response = client.get(
'/lab/vulnerable/orders/1002',
headers={'X-Lab-User': '1'},
)
assert response.status_code == 200
assert response.get_json()['owner_id'] == 2


@pytest.mark.parametrize(
'user_id, order_id, expected',
[
('1', 1001, 200),
('1', 1002, 404),
('2', 1001, 404),
('2', 1002, 200),
('1', 9999, 404),
],
)
def test_fixed_access_matrix(client, user_id, order_id, expected):
response = client.get(
f'/lab/fixed/orders/{order_id}',
headers={'X-Lab-User': user_id},
)
assert response.status_code == expected
if expected == 200:
data = response.get_json()
assert data['id'] == order_id
assert data['owner_id'] == int(user_id)
else:
assert response.get_json(silent=True) is None


@pytest.mark.parametrize('identity', [None, '3', 'not-a-user'])
def test_missing_or_invalid_identity(client, identity):
headers = {} if identity is None else {'X-Lab-User': identity}
response = client.get('/lab/fixed/orders/1001', headers=headers)
assert response.status_code == 401

执行:
python -m pytest -q

这些测试通过 Flask 测试客户端调用应用,不需要先启动监听端口。漏洞复现测试只用于说明实验现象,不应作为生产系统必须保留的行为。迁移到真实项目时,应移除漏洞路由,仅保留针对正式接口的安全回归测试。

上述内容是根据示例逻辑给出的预期结果,不是对任何外部系统的实测结论。

九、真实项目的修复方案:把授权条件推到数据访问边界

本实验为了便于阅读,先取对象再判断归属。数据库项目通常可以把访问范围直接写入查询条件,例如使用参数化查询:

SELECT id, owner_id, item
FROM orders
WHERE id = :eek:rder_id AND owner_id = :current_user_id;

参数必须绑定,不能通过字符串拼接生成 SQL。多租户系统还应约束 tenant_id,并从可信的认证上下文或服务端成员关系中取得租户范围,不能直接相信请求里传来的租户编号。

对于修改和删除,应在执行更新或删除的同一条语句中加入授权条件,并检查受影响行数。仅在修改前单独检查一次权限,可能遇到检查后对象归属发生变化的竞态问题。

还应检查下列容易遗漏的入口:
1. 列表接口:不能只保护详情,却在列表中返回其他用户的数据。
2. 批量接口:请求包含多个 ID 时,必须验证每个对象,明确部分失败还是整体拒绝。
3. 下载接口:数据库记录有权限控制,不代表文件静态地址也有控制。
4. 导出与异步任务:任务创建、执行、结果下载各阶段都需要匹配的授权策略。
5. 缓存:用户相关内容的缓存键必须体现相应权限范围,避免修复查询后仍从共享缓存泄露数据。

十、风险与注意事项

本实验的 X-Lab-User 头可以被任意修改,因此它只适合隔离实验。真实项目如果同样信任客户端传入的身份字段,还存在更基础的身份伪造风险,不能只补 owner_id 比较就宣告完成修复。

本文演示的是只读订单接口。对于支付、退款、删除等有状态操作,测试前应准备一次性数据、回滚方案和明确授权,优先使用自动化测试或隔离环境,不要为了证明问题而执行真实交易或破坏性操作。

报告中不应复制无关敏感字段。记录必要的身份编号、对象归属、请求路径、响应状态和经过脱敏的关键字段即可。生产日志也不宜记录完整令牌、会话 Cookie 或个人信息。

十一、常见问题

问:前端隐藏按钮或禁止编辑订单 ID 能解决吗?
答:不能。服务端必须把每次请求都视为可以被独立构造,前端限制不能替代授权。

问:换成随机 ID 是否足够?
答:不够。不可预测标识可以增加猜测成本,但无法证明请求者具有访问权限。

问:管理员是否也必须满足 owner_id 条件?
答:取决于明确的业务策略。若管理员确实可以跨用户访问,应在服务端单独表达角色、范围和审计要求,而不是通过客户端参数临时绕过检查。

问:HTTP 200 一定说明越权成功吗?
答:不一定。有些接口始终返回 200,再通过业务字段表示失败。必须检查实际响应内容和必要的状态变化,不能只看状态码。

问:能否用这份测试直接证明整个系统没有越权?
答:不能。它只覆盖示例中的身份、对象和读取操作。真实系统还需要按角色、租户、操作类型以及共享关系建立授权矩阵,并覆盖拒绝路径。

十二、总结

对象级授权缺失的关键不在于 ID 是否容易猜,而在于服务端是否把当前主体、目标对象和具体操作绑定起来验证。有效的研究与修复闭环应该包括:建立正常基线、使用两个已知归属的账号和对象进行最小对照、定位缺失条件、修复数据访问边界、补充正反向自动化测试。

一个可靠结论应当能够回答三个问题:谁访问了哪个对象,哪条授权规则本应阻止访问,以及修复后如何持续证明这条规则没有再次失效。
 

黄先生嗯

New member

黄先生嗯 Rep
0
0
0
Rep
0
黄先生嗯 Vouches
Vouches
0
1 MONTH
1 1 MONTH OF SERVICE
黄先生嗯 获得
0
0
0
获得
0
黄先生嗯 凭证
0
0
0
凭证
0
主题
15
点赞
0
HakB
1 月
在社区成长 1 月
LEVEL 1 1 XP
 
补充一个容易影响复现的细节:示例中的 Flask 路由需要写成 `/lab/vulnerable/orders/<int:eek:rder_id>` 和 `/lab/fixed/orders/<int:eek:rder_id>`,否则视图函数拿不到 `order_id`,请求可能直接返回 500。回归测试还应覆盖修改、删除、附件下载等接口;不要只测 GET,因为同一对象在读取接口已校验归属,不代表 PUT、DELETE 或下载接口也复用了相同授权策略。实践中可把“用户×对象×操作”整理成参数化权限矩阵,避免修复单一路由后留下旁路。
 
顶部