在生产环境中强化一个 Node API:正确的 CORS、速率限制、安全响应头,以及针对主要 OWASP API 风险的防御。
在 Kodokon 中打开本课OWASP 维护着一份专门针对 API 的 top 10。三大主要风险是:BOLA(Broken Object Level Authorization,对象级别授权失效 - 通过修改一个 id 来访问另一个用户的对象)、Broken Authentication(认证失效 - 校验不当的令牌、脆弱的密钥)和 Unrestricted Resource Consumption(不受限制的资源消耗 - 对速率、大小或时间没有任何限制)。请注意,这三者中没有一个能靠安装一个库来解决:它们是你设计方案的属性。让我们从贯穿全局的机制开始:CORS、速率限制和响应头。
import { createServer } from "node:http";
const allowed = new Set(["https://app.example.com"]);
const server = createServer((req, res) => {
const origin = req.headers.origin;
if (origin && allowed.has(origin)) {
res.setHeader(
"Access-Control-Allow-Origin",
origin,
);
res.setHeader("Vary", "Origin");
}
if (req.method === "OPTIONS") {
res.setHeader(
"Access-Control-Allow-Methods",
"GET,POST,PUT,DELETE",
);
res.setHeader(
"Access-Control-Allow-Headers",
"content-type,authorization",
);
res.setHeader("Access-Control-Max-Age", "86400");
res.writeHead(204).end();
return;
}
res.setHeader("Content-Type", "application/json");
res.end(JSON.stringify({ ok: true }));
});
server.listen(3000);要清楚地理解 CORS 不是什么:它不是对服务器的保护。curl 或第三方后端会完全无视这些响应头;只有浏览器才会强制执行它们,目的是保护它的用户。三条专家规则:从一个允许列表中返回精确的来源(绝不要盲目地回显 Origin 请求头)、加上 Vary: Origin 以免污染中间缓存,以及要知道一旦请求携带了 cookie(credentials),* 这个通配符就会被浏览器拒绝。
const WINDOW_MS = 60_000;
const LIMIT = 100;
const hits = new Map();
function check(ip) {
const now = Date.now();
const entry = hits.get(ip);
if (!entry || now - entry.start >= WINDOW_MS) {
hits.set(ip, { start: now, count: 1 });
return { ok: true, remaining: LIMIT - 1 };
}
entry.count += 1;
const remaining = Math.max(LIMIT - entry.count, 0);
return { ok: entry.count <= LIMIT, remaining };
}
console.log(check("203.0.113.7"));安全响应头只需几行代码,就能挡住一整类攻击:HSTS 会强制未来的访问使用 HTTPS,X-Content-Type-Options: nosniff 会阻止浏览器去猜测 MIME 类型,而一个严格的 CSP(default-src 'none')会让一个不慎在浏览器中被渲染的 API 响应无法被利用。同时也要移除任何暴露你技术栈的响应头:Express 默认会添加 X-Powered-By - 用 app.disable('x-powered-by') 把它禁用。
function setSecurityHeaders(res) {
res.setHeader(
"Strict-Transport-Security",
"max-age=63072000; includeSubDomains",
);
res.setHeader("X-Content-Type-Options", "nosniff");
res.setHeader(
"Content-Security-Policy",
"default-src 'none'; frame-ancestors 'none'",
);
res.setHeader("Cache-Control", "no-store");
}
export { setSecurityHeaders };