Укрепи Node-API в продакшене: правильный CORS, ограничение частоты запросов, заголовки безопасности и защита от главных рисков OWASP API.
Открыть этот урок в KodokonOWASP ведёт отдельный топ-10, посвящённый API. Три доминирующих риска: 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 из белого списка (никогда не отражай заголовок Origin вслепую), добавляй Vary: Origin, чтобы не отравить промежуточные кеши, и помни, что подстановочный знак * отвергается браузерами, как только запрос несёт куки (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 };