引言
不安全的 API,几乎从来不是写给“粗心开发者”的。
它们往往出自非常自信的开发者之手——
这些人遵循了“正常”的编码习惯,用了主流框架,也加了鉴权。
问题不在于“有没有做安全”,
而在于:太早相信安全已经做完了。
看起来一切都很正常
想象这样一个场景:
你有一个已经上线的 Node.js API。
从外部看,一切都很健康。
这正是大多数 Node.js API 的真实状态。
它们不是“坏掉的”。
它们只是——不安全。
而且这种不安全:
它会安静地躺在那里,
直到某个“不该来的请求”出现。
为什么 Node.js API 会让人“感觉安全”
因为现代框架真的很会营造安全感。
你用了:
- • Express / NestJS / Fastify
代码结构清晰、职责分明,看起来非常专业。
于是你会下意识认为:
安全这块,我已经 setup 过了。
这很自然。
框架隐藏了复杂度,中间件解决了难题。
但危险也正是从这里开始的。
因为大多数安全问题,并不是因为缺工具,
而是因为代码在工具之后,开始假设太多事情是“可信的”。
真正的问题:后端开始“信任客户端”
绝大多数不安全的 Node.js API,
不是被什么高级攻击击穿的。
而是因为——
后端慢慢开始相信客户端。
来看一个非常真实、非常常见的例子。
❌ 不安全的代码(极其常见)
// PUT /api/profile
app.put("/api/profile", authenticateUser, async (req, res) => {
const { userId, name, email } = req.body;
await User.update(
{ name, email },
{ where: { id: userId } }
);
res.json({ success: true });
});
为什么开发者会这样写?
真正的问题是什么?
后端信任了 userId 来自请求体。
攻击者只需要改一下请求:
{
"userId": "1234",
"name": "Hacked",
"email": "hacked@mail.com"
}
如果用户 1234 存在,
他的资料就被改了。
注意几个关键点:
安全是“静默失败”的。
✅ 正确的写法(后端必须决定身份)
app.put("/api/profile", authenticateUser, async (req, res) => {
const { name, email } = req.body;
const authenticatedUserId = req.user.id;
await User.update(
{ name, email },
{ where: { id: authenticatedUserId } }
);
res.json({ success: true });
});
一句话原则:
永远不要让客户端告诉你:它在“替谁”操作。
信任是如何一步步渗入代码的?
这些决定,单独看都不吓人:
它们:
但结果是:
后端慢慢不再是“事实来源”。
它不再验证,只是相信。
请把这句话读两遍:
后端开始“相信”,而不是“检查”。
客户端不是你系统的一部分
这一点非常重要,但经常被忽略:
除非你在代码里显式检查,
否则后端根本分不清“正常请求”和“恶意请求”。
而最危险的地方在于:
什么都不会立刻坏掉。
测试通过。
用户没感觉。
系统继续跑。
于是信任继续增长。
认证 ≠ 授权(最常见的误解)
这是 Node.js API 安全里最被误解的一点。
- • 授权(Authorization):你能干什么
很多 API,只回答了第一个问题。
❌ 不安全的代码(SaaS 中极其常见)
// DELETE /api/orders/:orderId
app.delete("/api/orders/:orderId", authenticateUser, async (req, res) => {
const { orderId } = req.params;
await Order.destroy({
where: { id: orderId }
});
res.json({ success: true });
});
为什么“感觉安全”?
真正缺了什么?
所有权校验。
API 从未问过一句:
这个订单,属于这个用户吗?
只要能猜到 ID,
任何登录用户都能删任何订单。
✅ 加上授权逻辑
app.delete("/api/orders/:orderId", authenticateUser, async (req, res) => {
const { orderId } = req.params;
const userId = req.user.id;
const order = await Order.findOne({
where: { id: orderId, userId }
});
if (!order) {
return res.status(403).json({ error: "Not allowed" });
}
await order.destroy();
res.json({ success: true });
});
现实中的不安全代码,通常“看起来很无聊”
比如数据泄露型问题:
app.get("/api/users/:id", authenticateUser, async (req, res) => {
const user = await User.findByPk(req.params.id);
res.json(user);
});
你可能无意中暴露了:
今天没事,
明天加一个字段就出事。
✅ 更安全的模式
app.get("/api/users/:id", authenticateUser, async (req, res) => {
const user = await User.findOne({
where: { id: req.params.id },
attributes: ["id", "name", "avatar"]
});
res.json(user);
});
为什么“以后再修”几乎等于不修?
安全债务和技术债务不一样。
直到它突然变成事故。
随着系统变大:
等你真的想修时,
系统已经脆弱到不敢动。
真正安全的 API 是怎么写出来的?
这不是偏执,
这是清晰。
而清晰,最终会让系统:
最后的真相
大多数不安全的 Node.js API,
不是出自粗心开发者。
而是出自能力很强、经验丰富、但过于信任自己工具和习惯的人。
Node.js API 安全不是恐惧。
也不是追求完美。
它是一种持续的意识。