Bug triage(漏洞分级)
团队聊天里冒出一条消息:
我怀疑客户能看到彼此的订单。
这可能很紧急,也可能只是一场误会。谁都还说不准。
漏洞分级(Bug triage) 就是从"有人发现了点什么"到"团队知道接下来该做什么"之间的这段工作。
对于安全漏洞,分级要回答四个问题:
- 我们能复现它吗?
- 它会造成什么危害?
- 利用它有多难?
- 接下来该怎么办?
复现这个漏洞
在有人能故意把它重现出来之前,一个安全发现只是一个"说法"。
先看看这个有问题的路由:
app.get('/orders/:id', async (req, res) => {
const order = await db.orders.findById(req.params.id)
res.json(order)
})现在把复现步骤写出来:
# 以客户 4102 的身份登录
curl -i https://app.example.com/orders/88213 \
-H "Cookie: sid=<session for customer 4102>"
# 预期结果:403 或 404
# 实际结果:200,返回了另一个客户的订单一份好的复现记录要包含账号、请求、预期结果和实际结果。
"改一下 id 就出问题了"只是一句备注。上面这段代码块,才是另一个开发者能直接跑起来的东西。
只在你有权限的地方测试
只对你拥有、维护,或已获得书面测试许可的系统进行复现操作。
对别人的应用发出同样的请求,那不叫分级,那是别的事。
把账号、请求、原本应该发生什么、实际发生了什么都写下来。如果另一个开发者不用问你就能照做,那这份报告就是有用的。
说明影响
**影响(Impact)**指的是利用这个漏洞的人,最终能拿到什么、能做什么。
对于这个订单漏洞,影响可以这样描述:
任何已登录的客户都能读取其他客户的收货地址和订单历史。
这句话比下面这句要好得多:
/orders/:id存在 IDOR。
IDOR 是这类问题的安全术语,全称是"不安全的直接对象引用"(insecure direct object reference)。它的意思是:请求里带着某个对象的 id,而服务器没有检查调用者是否有权使用这个对象。
术语名称有助于工程师对漏洞分类,而影响说明能让所有人都理解这为什么重要。
问自己:
- 暴露的是什么数据或操作?
- 这些数据或操作属于谁?
- 可能有多少人受影响?
- 这个问题接下来可能被用来做什么?
严重程度的判断,起点是造成的伤害,而不是漏洞本身有多"巧妙"。
安全术语很有用,但光靠术语远远不够。"任何客户都能读取其他客户的地址"比"IDOR"能让更多人明白问题所在。
判断可利用性
影响告诉你这个漏洞能造成什么后果,**可利用性(Exploitability)**告诉你需要付出多少代价才能达成它。
对于这个订单漏洞,攻击者需要:
- 一个普通客户账号
- 一个有效的或可猜测的订单 id
- 一次请求
这非常容易触达。而一个需要管理员账号、物理接触、精确时机加上受害者点击的漏洞,情况就完全不同了。
把这些前提条件写下来,能让"严重程度"这场讨论落到实处。
| 前提条件 | 含义 |
|---|---|
| 匿名 | 互联网上任何人都能尝试。 |
| 已登录 | 需要一个普通账号。 |
| 特权角色 | 需要员工或管理员角色。 |
| 受害者操作 | 需要别人点击或提交某些内容。 |
| 可猜测的 id | 目标可以通过尝试不同值找到。 |
| 时间窗口 | 漏洞只在一个很窄的时间段内成立。 |
匿名比登录更容易触达;登录比管理员更容易触达;一次请求比需要精确时机、还要有人点击某样东西的攻击链更容易触达。
决定接下来怎么做
现在把各个部分拼到一起:
| 字段 | 示例 |
|---|---|
| 复现步骤 | 客户 4102 请求订单 88213,收到了另一个客户的订单。 |
| 影响 | 任何已登录的客户都能读取其他客户的收货地址和订单历史。 |
| 可利用性 | 普通账号、可猜测的 id、一次请求。 |
| OWASP 分类 | A01 访问控制失效(Broken Access Control)。 |
| 下一步行动 | 发布前修复,添加所有权校验测试,检查类似路由。 |
OWASP 分类帮你给漏洞归类,下一步行动则把这份报告变成一项实际工作。
对于这个漏洞,修复方式是在返回订单之前先检查所有权:
app.get('/orders/:id', async (req, res) => {
const order = await db.orders.findById(req.params.id)
if (!order || order.userId !== req.user.id) {
return res.status(404).json({ error: 'Order not found' })
}
res.json(order)
})配套的测试应该验证的是"错误路径",而不只是"正常路径"。
一份好的分级记录会带来后续工作
修复这个接口。
添加一个回归测试。
在相邻的路由中排查是否存在同样的问题。
对于这个订单漏洞,决定应该很直接:发布前修复,添加测试,并检查相邻路由是否存在同样缺失的所有权检查。
动手试一试
一份漏洞报告只有一句话:"密码重置链接用过之后仍然有效。"
给它做分级。填出这五个字段:复现步骤、影响、可利用性、OWASP 分类、下一步行动。
对照你的答案
| 字段 | 一个合理的答案 |
|---|---|
| 复现步骤 | 申请一次重置,点击链接,设置一个新密码,然后再次打开同一个链接。它依然被接受,还能再设置一次密码。 |
| 影响 | 任何看到过这个链接的人,哪怕只看到一次,之后都能接管这个账号。重置链接会长期留在收件箱、浏览器历史记录和转发邮件里。 |
| 可利用性 | 不需要账号,也不需要猜测。它需要的只是拿到这个链接,所以难度完全取决于这个链接流传到了哪里。 |
| OWASP 分类 | A07 身份验证失败(Authentication Failures)。当一个已经用过的凭证仍然有效时,应用就无法可靠地确认"这个人到底是谁"。 |
| 下一步行动 | 让令牌在第一次使用后立即失效,如果还没有过期时间,加上一个。然后检查邮箱验证和邀请链接是否有同样的问题。 |
有两点值得特别拿出来说一下。
这里的"影响"不是"令牌没有失效",而是"有人能接管账号"。只是把漏洞本身复述一遍的字段没有任何价值,这一栏存在的意义,是要说清楚谁会因此受害。
而"下一步行动"最后落在了"往旁边看一看"上。一次性令牌通常是由共享代码生成的,所以一个流程里的漏洞很可能在其他流程里也存在。只修好一个路由就收工的分级,只完成了一半的工作。
接下来去哪儿
漏洞分级到此告一段落,这也是本书第一部分的结尾。现在你已经能够想清楚哪里可能出问题、说出常见的现实漏洞、并把一个发现转化为实际工作。
接下来,手册将进入输入与数据安全部分,从永远不要相信用户输入开始。

