如何思考安全问题
一个功能可以运行得完美无缺,却依然不安全。想象一个很小的订单接口:
app.get('/orders/:id', async (req, res) => {
const order = await db.orders.findById(req.params.id)
res.json(order)
})这个接口读取一个 id,找到对应订单并返回。如果你用自己的订单去测试,看起来一切正常。现在把 URL 里的 id 改一改。如果服务器从不检查这个订单归谁所有,一个已登录的用户就能读到另一个用户的账单。
这就是安全思维:不能只问代码能不能用,还要问别人能拿它做出什么事来。
安全考虑的是第二个问题:如果有人用一种我们没预料到的方式来用它,会发生什么?
留意边界
信任边界 指的是你所掌控的代码,和某个可以向你传入你没有选择过的值的地方之间的分界线。
浏览器就站在这条线的另一侧。用户可以修改 URL、编辑隐藏的表单字段、重复提交某个查询参数、删掉一个 cookie,甚至绕过你的前端直接手写请求发送过去。
这就决定了:浏览器不是做重要判断的地方。
浏览器端的检查有用,但它不是真正的把关点
客户端校验能帮用户更顺利地填表单,及早发现拼写错误,带来更好的体验。
但服务器仍然必须再检查一遍这个值,因为真正保护数据的是服务器。
下面是同一个订单接口,把缺失的判断明确加了回来:
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)
})关键的一行是 order.userId 和 req.user.id 的比较。服务器在表达这样一个意思:这个订单也许存在,但它还必须属于发起请求的这个人。
只有当服务器能自行证明这个判断成立,请求才应该被放行。
把重要的检查放在服务器上。那里才是你的应用能够拿请求和它已经信任的数据做对比的地方。
给可能出的问题起个名字
一旦你发现了一个有风险的判断,就给可能出现的失败起个名字:
| 问题 | 安全术语 | 例子 |
|---|---|---|
| 谁能读到这个? | 机密性(Confidentiality) | 一个用户读到了另一个用户的订单。 |
| 谁能改动这个? | 完整性(Integrity) | 一个用户在结账前改动了价格。 |
| 大家还能正常使用吗? | 可用性(Availability) | 一个请求就让应用对所有人都变卡。 |
这三者合在一起,就是所谓的 CIA 三要素:机密性、完整性和可用性。
泄露的账单是机密性问题。可被篡改的隐藏价格字段是完整性问题。一个允许单个请求查询上百万条记录的搜索接口,是可用性问题。
有些漏洞不止影响一个方面。如果一个用户能编辑另一个用户的收货地址,那他既能看到它,也能改动它。
这几个问题能把一种模糊的担忧,变成一件你能说清楚的事情。当你能给伤害起个名字,一份 bug 报告就会清晰得多。
换位思考
"像攻击者一样思考"这句话听起来有点戏剧化。放到日常开发里,其实要平淡得多。挑一个输入,问自己:
- 如果这个值缺失了会怎样?
- 如果它比预期长得多会怎样?
- 如果它的类型不对会怎样?
- 如果它属于另一个用户会怎样?
- 如果同一个请求被发送了成千上万次会怎样?
拿一个个人资料表单来说。页面上可能有一个 displayName 的文本输入框、一个隐藏的 userId,还有一个提交按钮。原本设想的路径很简单:已登录用户更新自己的名字。
但安全的思路会问一些不同的问题。userId 为什么会出现在表单里?如果它指向别人会怎样?服务器用的是已登录的会话信息,还是直接相信了提交上来的这个字段?
隐藏不等于可信
一个隐藏的表单字段,只是对页面隐藏,并不是对发送请求的人隐藏。
如果这个值很重要,就应该在服务器端从会话或数据库里重新推导出来。不要指望浏览器把一个服务器早就知道的判断结果原样送回来。
同样的习惯在表单之外也适用。文件上传会声明一个文件名和内容类型。令牌会声明一个身份。webhook 会声明某个事件发生了。在你的代码即将依赖某个声明之前,先检查这个声明本身。
这就是安全思维的一个简单、可上手的版本。
接下来往哪走
这种思维方式,正是为课程接下来的内容做铺垫。
威胁建模要在功能上线之前就问清楚可能出什么问题。OWASP 为线上应用中的各类漏洞提供了一套通用的命名方式。bug 分级则能帮你决定先修哪些问题。
把这个循环保持得小而清晰:
- 找到边界。
- 给可能出的问题起个名字。
- 在服务器端检查这个声明。

