本篇只研究两个很小的边界:通过身份认证后是否还需要检查对象归属;输入字符串进入 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' \ |
预期 200,owner 为 alice。再去掉请求头,预期 401。这是“能识别身份”的第一道检查。
2.2 漏洞版只检查登录,没有检查对象归属
curl -i -H 'Authorization: Bearer training-alice' \ |
预期仍然 200,但 owner 为 bob。证据是 Alice 的身份 + Bob 的对象 + 服务端成功返回数据。仅仅修改 URL 中的编号不是漏洞结论,越过了原本不应越过的授权边界才是。
2.3 相同身份和对象,访问修复版
curl -i -H 'Authorization: Bearer training-alice' \ |
预期分别是 403 和 200。合法使用仍成功,跨用户访问被拒绝。这比“所有请求都拒绝了”更能证明修复符合业务要求。
关键判断可以写成:
user = authenticated_user # 来自服务端验证过的身份 |
真实系统可以在数据库查询时同时限定 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( |
这段 ? 占位符及第二参数是 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,写一份包含“证据—根因—修复—回归”的短记录。返回学习路线。返回分类。