Skip to content
This page has been auto-translated and may contain errors.View in English

Bug triage(漏洞分级)

团队聊天里冒出一条消息:

我怀疑客户能看到彼此的订单。

这可能很紧急,也可能只是一场误会。谁都还说不准。

漏洞分级(Bug triage) 就是从"有人发现了点什么"到"团队知道接下来该做什么"之间的这段工作。

对于安全漏洞,分级要回答四个问题:

  1. 我们能复现它吗?
  2. 它会造成什么危害?
  3. 利用它有多难?
  4. 接下来该怎么办?

复现这个漏洞

在有人能故意把它重现出来之前,一个安全发现只是一个"说法"。

先看看这个有问题的路由:

Vulnerable
js
app.get('/orders/:id', async (req, res) => {
  const order = await db.orders.findById(req.params.id)
  res.json(order)
})

现在把复现步骤写出来:

bash
# 以客户 4102 的身份登录
curl -i https://app.example.com/orders/88213 \
  -H "Cookie: sid=<session for customer 4102>"

# 预期结果:403 或 404
# 实际结果:200,返回了另一个客户的订单

一份好的复现记录要包含账号、请求、预期结果和实际结果。

"改一下 id 就出问题了"只是一句备注。上面这段代码块,才是另一个开发者能直接跑起来的东西。

只在你有权限的地方测试

只对你拥有、维护,或已获得书面测试许可的系统进行复现操作。

对别人的应用发出同样的请求,那不叫分级,那是别的事。

Juno复现这个漏洞 复现步骤就是让漏洞再次发生的"菜谱"。

把账号、请求、原本应该发生什么、实际发生了什么都写下来。如果另一个开发者不用问你就能照做,那这份报告就是有用的。

Juno复现这个漏洞 把漏洞压缩成还能证明问题存在的最小请求。去掉多余的请求头、步骤和数据,直到再拿掉任何一样东西,这个行为就消失为止。

这样一个更小的请求更容易测试、更容易修复,也更难被人当作"没那么严重"打发过去。它还能告诉你,系统里到底是哪个环节做出了不安全的判断。

Juno复现这个漏洞 对于疑似漏洞,先复现。对于已经被利用的漏洞,先控制局面。

如果日志显示有陌生账号在逐个尝试订单 id,或者客户反馈看到了不属于自己的数据,那就先保存证据、收紧访问权限。

别把事故处理中最宝贵的一小时,花在向自己证明这个漏洞确实存在上。复现可以等一等,只要日志还没消失——而日志特别喜欢消失,这大概是它们最不讨人喜欢的习惯之一。

说明影响

**影响(Impact)**指的是利用这个漏洞的人,最终能拿到什么、能做什么。

对于这个订单漏洞,影响可以这样描述:

任何已登录的客户都能读取其他客户的收货地址和订单历史。

这句话比下面这句要好得多:

/orders/:id 存在 IDOR。

IDOR 是这类问题的安全术语,全称是"不安全的直接对象引用"(insecure direct object reference)。它的意思是:请求里带着某个对象的 id,而服务器没有检查调用者是否有权使用这个对象。

术语名称有助于工程师对漏洞分类,而影响说明能让所有人都理解这为什么重要。

问自己:

  • 暴露的是什么数据或操作?
  • 这些数据或操作属于谁?
  • 可能有多少人受影响?
  • 这个问题接下来可能被用来做什么?

严重程度的判断,起点是造成的伤害,而不是漏洞本身有多"巧妙"。

Juno说明影响 影响就是用大白话说清楚危害是什么。对方能读到、能改动、能做到什么?

安全术语很有用,但光靠术语远远不够。"任何客户都能读取其他客户的地址"比"IDOR"能让更多人明白问题所在。

Juno说明影响 如果你不知道准确数字,给一个大致范围也行。"三月以来的所有订单,大约 4 万条",总比这一栏空着要好。

然后把危害往下追一步。一个暴露的邮箱地址会让钓鱼攻击更容易得手,一个暴露的重置令牌可能演变成账号被接管。追一步能让报告立足于现实;追五步,就变成了带着严重程度标签的同人小说。

Juno说明影响 有些发现今天确实没有可触达的影响,硬要给它编出一个影响,只会让整个待处理列表变得更糟。某个不接受跨站写入的路由上缺了一个 cookie 标志,这是个真实的缺口,但眼下并不构成一个真实可行的账号接管故事。

把它记录为"目前没有可触达的影响",然后写清楚在什么情况下它会变得重要。这个"未来的条件"才是有价值的部分——它是给六个月后那个人留下的线索:当路由发生变化时,这个曾经被判定为低风险的发现,可能一下子就不再是可以忽略的了。

判断可利用性

影响告诉你这个漏洞能造成什么后果,**可利用性(Exploitability)**告诉你需要付出多少代价才能达成它。

对于这个订单漏洞,攻击者需要:

  • 一个普通客户账号
  • 一个有效的或可猜测的订单 id
  • 一次请求

这非常容易触达。而一个需要管理员账号、物理接触、精确时机加上受害者点击的漏洞,情况就完全不同了。

把这些前提条件写下来,能让"严重程度"这场讨论落到实处。

前提条件含义
匿名互联网上任何人都能尝试。
已登录需要一个普通账号。
特权角色需要员工或管理员角色。
受害者操作需要别人点击或提交某些内容。
可猜测的 id目标可以通过尝试不同值找到。
时间窗口漏洞只在一个很窄的时间段内成立。
Juno判断可利用性 可利用性就是这个漏洞成立之前,必须成立的那些条件的清单。

匿名比登录更容易触达;登录比管理员更容易触达;一次请求比需要精确时机、还要有人点击某样东西的攻击链更容易触达。

Juno判断可利用性 把这些前提条件当作需要核实的说法。如果报告说"需要管理员权限",就去查一查有多少个管理员账号,以及这个角色是怎么授予的。

如果报告说 id 无法被猜测,就去看看真实的 id 长什么样。一个末尾带计数器的时间戳,不会因为变量取了个好听的名字就变得不可猜测。命名这件事,坑过的可比我们厉害的人多了去了。

Juno判断可利用性 当周围的系统发生变化时,可利用性也会跟着变。一个因为"需要员工账号"而被评为低风险的发现,一旦支持工具向合作伙伴开放,就变成了一个完全不同的发现。

把日期和理由记在评级旁边。未来的你,或者经过三次组织重组之后接手这个服务的人,需要知道当初这个评级是基于哪个假设成立的。假设这种东西,在服务器机房里放坏得跟牛奶一样快。

决定接下来怎么做

现在把各个部分拼到一起:

字段示例
复现步骤客户 4102 请求订单 88213,收到了另一个客户的订单。
影响任何已登录的客户都能读取其他客户的收货地址和订单历史。
可利用性普通账号、可猜测的 id、一次请求。
OWASP 分类A01 访问控制失效(Broken Access Control)。
下一步行动发布前修复,添加所有权校验测试,检查类似路由。

OWASP 分类帮你给漏洞归类,下一步行动则把这份报告变成一项实际工作。

对于这个漏洞,修复方式是在返回订单之前先检查所有权:

Fixed
js
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)
})

配套的测试应该验证的是"错误路径",而不只是"正常路径"。

一份好的分级记录会带来后续工作

修复这个接口。

添加一个回归测试。

在相邻的路由中排查是否存在同样的问题。

Juno决定接下来怎么做 分级最终要落到一个决定上:现在修,带着理由稍后修,或者有意接受这个风险。

对于这个订单漏洞,决定应该很直接:发布前修复,添加测试,并检查相邻路由是否存在同样缺失的所有权检查。

Juno决定接下来怎么做 后续排查跟修复本身同等重要。某个路由上的访问控制失效漏洞,往往提示你该去搜一搜同一资源的其他部分了。

找一找那些读取 req.params.id、却没有比对所有权、角色或租户就返回或修改数据的处理函数。这样的排查,能把一份报告变成一整类漏洞的修复。

Juno决定接下来怎么做 修复分三层:修好这个路由,用回归测试把这个漏洞锁死,再去别处找找有没有同样的模式。跳过第三层,你就会在别的 URL 上重新发现同一个漏洞,还得再失望一次。

响应方式的选择也要小心。对不存在的订单和未授权的订单都返回 404,能隐藏这个 id 是否存在;返回 403 对客户端来说更清楚,但可能会泄露"这条记录确实存在"这个信息。要有意识地选定一种行为,并在同一资源上保持一致。

动手试一试

一份漏洞报告只有一句话:"密码重置链接用过之后仍然有效。"

给它做分级。填出这五个字段:复现步骤、影响、可利用性、OWASP 分类、下一步行动。

对照你的答案
字段一个合理的答案
复现步骤申请一次重置,点击链接,设置一个新密码,然后再次打开同一个链接。它依然被接受,还能再设置一次密码。
影响任何看到过这个链接的人,哪怕只看到一次,之后都能接管这个账号。重置链接会长期留在收件箱、浏览器历史记录和转发邮件里。
可利用性不需要账号,也不需要猜测。它需要的只是拿到这个链接,所以难度完全取决于这个链接流传到了哪里。
OWASP 分类A07 身份验证失败(Authentication Failures)。当一个已经用过的凭证仍然有效时,应用就无法可靠地确认"这个人到底是谁"。
下一步行动让令牌在第一次使用后立即失效,如果还没有过期时间,加上一个。然后检查邮箱验证和邀请链接是否有同样的问题。

有两点值得特别拿出来说一下。

这里的"影响"不是"令牌没有失效",而是"有人能接管账号"。只是把漏洞本身复述一遍的字段没有任何价值,这一栏存在的意义,是要说清楚谁会因此受害。

而"下一步行动"最后落在了"往旁边看一看"上。一次性令牌通常是由共享代码生成的,所以一个流程里的漏洞很可能在其他流程里也存在。只修好一个路由就收工的分级,只完成了一半的工作。

接下来去哪儿

漏洞分级到此告一段落,这也是本书第一部分的结尾。现在你已经能够想清楚哪里可能出问题、说出常见的现实漏洞、并把一个发现转化为实际工作。

接下来,手册将进入输入与数据安全部分,从永远不要相信用户输入开始。