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

基于功能的威胁建模

某学生门户网站新增了一个上传功能。学生可以提交作业,老师可以留下反馈。

这功能听起来小到很容易开发。一个表单、一个文件输入框、一个提交按钮,再加上一个老师查看的页面。

它同样小到可以被认真地做一次威胁建模。

**基于功能的威胁建模**会拿出一个功能,从头到尾走一遍流程。每走一步,你都要问:这里可能出什么问题?代码需要检查什么?

梳理功能流程

先从正常流程说起:

  1. 学生登录。
  2. 学生上传一个文件。
  3. 服务器存储该文件。
  4. 老师打开这份提交。
  5. 老师留下反馈。
  6. 学生阅读反馈。

这份清单就是你的地图。一开始不需要多复杂的图表。

现在标出每一个值跨越信任边界的地方:

步骤跨越边界的值该问的问题
上传文件名、文件类型、文件大小服务器有没有检查文件本身?
存储路径和元数据一个学生能不能覆盖另一个学生的文件?
老师查看提交 id这位老师是否被分配给这个学生?
反馈反馈文本显示时会不会变成脚本?
学生查看反馈 id一个学生能不能读到另一个学生的反馈?

这样一来,这个功能就有了清晰的轮廓,你也能看出安全决策该放在哪里。

先讲故事,再补威胁

如果一上来就列威胁名称,练习会显得很抽象。

如果先从功能的故事讲起,每个威胁都能落到具体的地方。

Juno梳理功能流程 先讲正常的故事。谁登录了?他们发送了什么?这些数据去了哪里?之后谁会读到它?

然后标出你的应用接收到"自己没有主动选择的东西"的那些时刻。那些地方就是服务器需要多问几个问题的地方。

Juno梳理功能流程 实际做法是把功能拆成一行行:步骤、值、边界、检查。这种格式能让模型贴近实际代码。

对于上传功能来说,重点关注的是文件本身、存储路径、提交 id、老师的反馈以及学生的查看权限。每一项都对应一个可以测试的处理逻辑或存储决策。

Juno梳理功能流程 基于功能的模型之所以好用,是因为它小到能做完。但代价是,它会继承系统里已经存在的结构性问题。

如果每个功能都依赖同一套薄弱的角色模型,你会不断在不同的外壳下发现同一个威胁。

这时候,功能模型的使命已经完成了——它正在告诉你,该安排一次更大范围的系统排查了。是挺烦人的,但总比把第五次重复出现的问题当成新发现要划算得多。

把威胁变成检查项

功能模型不应该只停留在一堆担忧上。

对于上传功能来说,一个担忧可能是:

学生可能上传不安全的东西。

这话没错,但太模糊了,没法据此开发。把它变成检查项:

  • 存储前先限制文件大小。
  • 只允许预期的文件类型。
  • 存储的文件名由服务器生成。
  • 文件存放在公开应用目录之外。
  • 显示文件前先检查这位老师是否被分配给该学生。

现在这个模型就能变成验收标准了。

当下一位开发者能把某个威胁直接转化为代码或测试时,这个威胁才真正有用。

Juno把威胁变成检查项 担忧说的是什么让你害怕,检查项说的是应用打算怎么应对。

"上传不安全"是个担忧。"限制文件大小,并由服务器生成存储的文件名"才是一个可以被开发出来的检查项。

Juno把威胁变成检查项 用工单里同样的语言来写检查项。这样模型在会议结束后依然管用。

举个例子:只有当服务器确认这位老师被分配给了该学生,老师才能查看这份提交。这句话可以直接变成一个路由检查和一个测试用例,不需要再做任何转换。

Juno把威胁变成检查项 检查逻辑必须放在真正做决策的地方。老师界面里禁用的按钮固然有帮助,但它不是真正的控制点。提供该文件的那个路由才是控制点。

这正是老系统开销变大的地方。一个功能可能有三条路径通向同一份数据:网页路由、后台管理工具,还有后台导出任务。如果检查只放在其中一条路径上,那就相当于模型找出了一扇门,而实现却只装了一道帘子。

在老师反馈功能上试一试

现在轮到你自己用同样的方法来练习了。

这个功能是这样的:老师可以对学生的提交留下反馈。

在往下读之前,先回答这五个基于功能的问题。

  1. 谁发送这个值?
  2. 之后谁会读到它?
  3. 哪个决策很重要?
  4. 可能出什么问题?
  5. 代码应该检查什么?
对照你的答案
问题一个合理的答案
谁发送这个值?老师发送反馈文本。
之后谁会读到它?学生,也可能是另一位老师。
哪个决策很重要?只有被分配的老师才能留下反馈。
可能出什么问题?反馈里可能藏有脚本,或者关联到错误的提交上。
代码应该检查什么?在服务器端检查分配关系、文本长度,以及显示时的输出安全。

第2个问题分量最重,却最容易被忽略。说清楚"之后谁会读到这个值",才能让你意识到它会被渲染进页面里,也正因为如此,"可能藏有脚本"才是一个值得问的问题。

只回答第1个问题,你得到的是输入校验。把第2个问题也一起回答,你还能得到输出安全。

具体的修复方法会在手册后面讲到。现在重要的是养成习惯:梳理功能、标出边界、写出检查项。这就是这套方法的全部内容。

前端可以引导,但决定权在服务器

页面可以对没有被分配的老师隐藏反馈操作控件。

但路由仍然必须检查分配关系,因为任何人都可以直接调用这个路由。

Juno在老师反馈功能上试一试 老师的反馈看起来只是一个文本框里的文字,但这个功能背后藏着好几个决策。

谁可以写反馈?它属于哪份提交?之后谁可以读到它?安全思维就是在代码悄悄替你做出这些决定之前,先自己把它们找出来。

Juno在老师反馈功能上试一试 反馈功能常见的漏洞有两种形态:不该写的人写了,或者本该正常的文本在之后显示时变得不安全。

这就对应两处不同位置的检查。授权检查属于保存反馈的那个路由;输出安全属于渲染反馈的地方。同一个值,去往不同的地方,就要遵循不同的规则。

Juno在老师反馈功能上试一试 存储起来的文本很有耐心。它可以在数据库里安安静静地待上好几个月,然后在某个新页面把它渲染出来的那一刻,突然变得危险。

这就是为什么功能模型应该标注数据"要去哪里",而不只是"从哪里来"。"反馈文本会展示给学生和老师看",这句话比"反馈文本被存储起来"要有用得多。后一句话告诉你数据在哪里沉睡,前一句话告诉你它会在哪里醒来,然后毁掉你一个下午。

接下来往哪走

基于功能的威胁建模,是 STRIDE 的日常简化版。你可以在规划阶段直接用它,把检查项写进工单,并在功能上线前完成测试。

接下来,OWASP 与真实漏洞会把时间线倒过来。它不再是让你在发布前预想可能出什么问题,而是给那些已经在真实应用里出过问题的东西一一命名。