网络安全 04:对象越权、SQL 注入与修复验证

本篇只研究两个很小的边界:通过身份认证后是否还需要检查对象归属;输入字符串进入 SQL 时是否仍然只被当成数据。每个问题都先看漏洞版,再用同一用例检查修复版。

实验里的 Alice、Bob 和订单都是虚构数据。HTTP 服务只有本机端口;SQL 示例在断网容器里离线执行,没有网络 SQL 查询接口。先完成环境准备。

一、认证与授权分别问什么

认证回答“你是谁”,授权回答“你可以对哪个对象做什么”。成功登录不等于可以读所有订单;按钮被隐藏也不等于服务端已经阻止访问。

配套代码中,服务端把固定教学 token 映射为用户,再查订单归属。客户端不通过 ?user=alice 自报身份。生产系统应使用实际的登录和会话验证流程;本例省略了密码与 token 签发,只保留与对象检查有关的部分。

订单 ID 归属 内容
101 alice Notebook
102 bob Pencil

二、实验 A:已认证用户访问另一个用户的对象

2.1 正常读取自己的订单

curl -i -H 'Authorization: Bearer training-alice' \
http://127.0.0.1:18080/fixed/orders/101

预期 200,owner 为 alice。再去掉请求头,预期 401。这是“能识别身份”的第一道检查。

2.2 漏洞版只检查登录,没有检查对象归属

curl -i -H 'Authorization: Bearer training-alice' \
http://127.0.0.1:18080/lesson/orders/102

预期仍然 200,但 owner 为 bob。证据是 Alice 的身份 + Bob 的对象 + 服务端成功返回数据。仅仅修改 URL 中的编号不是漏洞结论,越过了原本不应越过的授权边界才是。

2.3 相同身份和对象,访问修复版

curl -i -H 'Authorization: Bearer training-alice' \
http://127.0.0.1:18080/fixed/orders/102

curl -i -H 'Authorization: Bearer training-bob' \
http://127.0.0.1:18080/fixed/orders/102

预期分别是 403 和 200。合法使用仍成功,跨用户访问被拒绝。这比“所有请求都拒绝了”更能证明修复符合业务要求。

关键判断可以写成:

user = authenticated_user   # 来自服务端验证过的身份
order = orders[order_id]
if order['owner'] != user:
return forbidden()
return order

真实系统可以在数据库查询时同时限定 ID 与用户、租户或角色范围,避免先拿到全量对象再遗漏检查。难猜的 UUID 可能减少编号猜测,但不能替代对象授权。OWASP IDOR 防护说明

三、实验 B:SQL 输入为什么变成了代码

先预测这条错误写法会怎样拼接字符串:

sql = "SELECT name FROM products WHERE name = '" + value + "'"

正常输入 Notebook 时,看起来能工作。若输入中含有 SQL 语法,查询的结构可能改变。本地离线演示准备了两条商品:Notebook 和 Pencil。

./bin/site.sh security-sql

程序依次对普通输入和教学输入 ' OR 1=1 -- 执行漏洞版与修复版,输出 JSON。

输入 漏洞版结果 修复版结果
Notebook 1 条 Notebook 1 条 Notebook
教学输入 2 条商品 空列表

在漏洞版中,输入关闭原来的字符串,再把条件改成恒真,注释掉剩余部分。修复版把它作为一个完整的商品名字去匹配,没有这个名字,自然返回空列表。

con.execute(
'SELECT name FROM products WHERE name = ?',
(value,)
)

这段 ? 占位符及第二参数是 Python sqlite3 的参数绑定机制。(value,) 是只有一个元素的元组,不是多余的标点。它将语句结构与数据值分开。Python sqlite3 官方文档

参数化主要解决值的位置;动态表名、列名和排序关键字需要通过固定映射等方式约束,不能认为把所有位置换成 ? 就会正确工作。过滤几个“危险单词”、自己拼转义规则,都不能替代可靠的参数绑定。OWASP SQL 注入防护

四、从请求回到日志

./bin/site.sh security-logs

程序每次 GET 输出一条结构化记录,含时间、方法、路径、状态码和识别到的用户,不记录 Authorization token。

{"time": 1790000000.0, "method": "GET", "path": "/fixed/orders/102", "status": 403, "user": "alice"}

上面是字段格式示意,时间不是本次运行的真实证据。练习时把自己的响应和同一次请求的日志对应起来。

复盘至少回答:发生在什么时间、使用哪个身份、访问哪个对象、服务端作出了什么决定。一次 403 可以由练习、误操作或恶意请求触发,不能仅凭这一条日志判定攻击意图。这个小服务没有请求 ID,若并发请求难以对应,下一步可以添加由服务端生成的 request_id。

五、把一次手工结果变成回归测试

./bin/site.sh security-test

七项测试覆盖:服务健康、缺失/错误身份、合法所有者、漏洞与修复的越权对照、错误/不存在的 ID、SQL 参数化,以及普通用户文件权限。测试不是“所有安全问题都没有”的证明,只证明这些已写明的用例满足预期。

练习扩展:添加第三个用户和第三条订单,然后写出完整的用户×订单访问矩阵。不要只补一个管理员成功用例;还要断言普通用户仍不能跨越对象边界。

六、浏览器安全接下来学什么

Cookie 保存状态,但不自动完成授权。HttpOnly 限制脚本读取 Cookie;Secure 约束 Cookie 的传输;SameSite 影响跨站请求携带 Cookie 的行为。这些属性各有作用,不能互相替代。本实验没有实现 Cookie 会话,所以不会把它的测试结果说成已覆盖 CSRF 或会话安全。MDN Cookie 指南

后续另建 XSS、CSRF 和会话实验时,应分别固定输出上下文、请求来源与状态变化,再设计修复前后的断言。

自测:把订单 ID 从数字换成 UUID,这次漏洞就消失了吗?

没有。只要知道一个不属于自己的有效 ID,仍可能读取对象。应把当前身份与对象访问策略绑定;编号难猜只是额外减少可发现性。

自测:修复后合法请求也返回 403,能否算完成?

不能。修复同时要满足合法请求成功和越界请求失败;否则只是把业务关闭了。

完成后执行 ./bin/site.sh security-down,写一份包含“证据—根因—修复—回归”的短记录。返回学习路线。返回分类。

知识共享许可协议