Пошук уроків, статей та іншого контенту
Додамо HTTP-заголовки для захисту Node.js API від небезпечних джерел, підміни типів і витоку службової інформації.
HTTP-заголовки безпеки повідомляють браузеру та іншим клієнтам, як обробляти відповідь API. Вони не замінюють автентифікацію, авторизацію чи перевірку вхідних даних, але зменшують наслідки типових помилок.
Для Node.js API найчастіше корисні такі заголовки:
Content-Security-Policy — обмежує джерела, з яких браузер може завантажувати ресурси.
X-Content-Type-Options: nosniff — забороняє браузеру вгадувати тип вмісту.
Strict-Transport-Security — змушує браузер використовувати HTTPS.
X-Frame-Options і директива frame-ancestors — забороняють вбудовувати відповідь у фрейм.
Referrer-Policy — обмежує передавання URL у заголовку Referer.
Permissions-Policy — вимикає непотрібні можливості браузера.
Cache-Control — забороняє кешування чутливих відповідей.
Відсутність службових заголовків — не розкриває технологію та внутрішню інформацію про сервер.
Заголовки потрібно встановити до відправлення тіла відповіді. У вбудованому модулі http це можна зробити через response.setHeader()
Нижче наведено невеликий API без сторонніх бібліотек:
const http = require("node:http");
const port = 3000;
function applySecurityHeaders(request, response) {
response.setHeader(
"Content-Security-Policy",
"default-src 'none'; frame-ancestors 'none'; object-src 'none'; base-uri 'none'"
);
response.setHeader("X-Content-Type-Options", "nosniff");
response.setHeader("X-Frame-Options", "DENY");
response.setHeader("Referrer-Policy", "no-referrer");
response.setHeader(
"Permissions-Policy",
"camera=(), microphone=(), geolocation=()"
);
// Цей API повертає приватні дані, тому відповіді не можна кешувати.
response.setHeader("Cache-Control", "no-store");
// Не додаємо заголовки, які розкривають реалізацію сервера.
response.removeHeader("X-Powered-By");
response.removeHeader("Server");
const isDirectHttps = request.socket.encrypted === true;
const isHttpsBehindTrustedProxy =
process.env.TRUSTED_PROXY === "1" &&
request.headers["x-forwarded-proto"] === "https";
if (isDirectHttps || isHttpsBehindTrustedProxy) {
response.setHeader(
"Strict-Transport-Security",
"max-age=31536000"
);
}
}
function sendJson(request, response, statusCode, payload) {
applySecurityHeaders(request, response);
const body = JSON.stringify(payload);
response.statusCode = statusCode;
response.setHeader("Content-Type", "application/json; charset=utf-8");
response.setHeader("Content-Length", Buffer.byteLength(body));
response.end(body);
}
const server = http.createServer((request, response) => {
if (request.method === "GET" && request.url === "/health") {
sendJson(request, response, 200, { status: "ok" });
return;
}
if (request.method === "GET" && request.url === "/api/profile") {
// У реальному API тут спочатку має виконуватися перевірка доступу.
sendJson(request, response, 200, {
id: 42,
name: "Olena"
});
return;
}
sendJson(request, response, 404, {
error: "Not Found"
});
});
server.listen(port, () => {
console.log(`API запущено на http://localhost:${port}`);
});Запустіть сервер:
node server.jsПеревірити заголовки можна за допомогою curl:
curl -i http://localhost:3000/api/profileУ відповіді будуть, зокрема:
Content-Security-Policy: default-src 'none'; frame-ancestors 'none'; object-src 'none'; base-uri 'none'
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Referrer-Policy: no-referrer
Permissions-Policy: camera=(), microphone=(), geolocation=()
Cache-Control: no-store
Content-Type: application/json; charset=utf-8Content-Security-Policy описує, звідки браузер може завантажувати скрипти, стилі, зображення та інші ресурси.
Для API, яке повертає лише JSON і не містить HTML, можна використати сувору політику:
default-src 'none'; frame-ancestors 'none'; object-src 'none'; base-uri 'none'Її значення:
default-src 'none' — заборонити всі джерела за замовчуванням;
frame-ancestors 'none' — заборонити вбудовування відповіді у iframe;
object-src 'none' — заборонити старі плагіни через object і embed;
base-uri 'none' — заборонити зміну базової URL-адреси документа.
CSP особливо важлива для HTML-відповідей. Для JSON API вона не замінює перевірку даних або захист від несанкціонованих запитів, але допомагає, якщо відповідь помилково обробляється браузером як документ.
Не слід без потреби використовувати надто широкі правила:
script-src *Таке правило дозволяє завантажувати скрипти з будь-якого джерела та послаблює захист від XSS.
Заголовок:
X-Content-Type-Options: nosniffзабороняє браузеру самостійно вгадувати MIME-тип відповіді.
API має явно вказувати тип даних:
response.setHeader("Content-Type", "application/json; charset=utf-8");Без nosniff браузер у деяких випадках може спробувати обробити відповідь не як JSON, а як інший тип вмісту. Це особливо небезпечно, якщо сервер повертає користувацькі дані або помилкові відповіді з неправильним Content-Type.
Заголовок Strict-Transport-Security має вигляд:
Strict-Transport-Security: max-age=31536000Після отримання цього заголовка браузер протягом указаного часу замінює HTTP-запити до домену на HTTPS.
HSTS потрібно використовувати лише тоді, коли:
HTTPS уже налаштований;
усі потрібні маршрути доступні через HTTPS;
домен не повинен працювати через звичайний HTTP.
Якщо додати HSTS до локального HTTP-сервера під час розробки, браузер може запам’ятати це правило для localhost, що ускладнить подальше тестування.
Якщо TLS завершується на reverse proxy, наприклад на балансувальнику, Node.js може отримувати внутрішній HTTP-запит. У такому разі X-Forwarded-Proto можна враховувати лише від довіреного proxy. Не можна безумовно довіряти цьому заголовку, якщо клієнт може передати його безпосередньо.
У прикладі це контролюється змінною середовища:
TRUSTED_PROXY=1 node server.jsВикористовуйте це лише за наявності справді довіреного reverse proxy.
Для сумісності з різними браузерами можна використовувати обидва заголовки:
X-Frame-Options: DENY
Content-Security-Policy: frame-ancestors 'none'X-Frame-Options — старіший механізм, а frame-ancestors є директивою CSP. Разом вони забороняють показувати відповідь у iframe.
Для API це зазвичай не основний сценарій атаки, але такі заголовки створюють безпечніший захист за замовчуванням, якщо endpoint пізніше почне повертати HTML.
Не варто повідомляти клієнту:
стек викликів;
абсолютні шляхи до файлів;
текст SQL-запитів;
змінні середовища;
назви внутрішніх сервісів;
версії бібліотек без необхідності.
Небезпечний варіант обробки помилки:
response.end(JSON.stringify({
error: error.stack
}));Безпечніший варіант:
console.error(error);
response.statusCode = 500;
response.setHeader("Content-Type", "application/json; charset=utf-8");
response.end(JSON.stringify({
error: "Internal Server Error"
}));Деталі помилки потрібно записувати у внутрішні журнали, а клієнту повертати загальне повідомлення.
Якщо використовується Express, за замовчуванням він може додавати заголовок X-Powered-By: Express. Його потрібно вимкнути:
app.disable("x-powered-by");Сам по собі цей заголовок не створює вразливість, але повідомляє потенційному атакувальнику зайву інформацію про стек застосунку.
Чутливі відповіді не повинні зберігатися в кешах браузера або проміжних proxy:
Cache-Control: no-storeЦе доречно для:
профілю користувача;
токенів;
платіжної інформації;
персональних даних;
відповідей, що залежать від автентифікації.
Для загальнодоступних незмінних ресурсів no-store може бути зайвим, але для приватних endpoint це безпечний варіант за замовчуванням.
HSTS призначений для HTTPS. Не вмикайте його безумовно на локальному сервері.
X-Content-Type-Options без правильного Content-Typenosniff не визначає тип відповіді самостійно. Сервер також має встановлювати правильний Content-Type.
Правила на кшталт default-src * або script-src * майже не обмежують небезпечні джерела. Для JSON API краще починати з default-src 'none'.
CSP контролює поведінку браузера щодо ресурсів, а CORS визначає, які вебсайти можуть читати відповіді API через браузер. Це різні механізми.
Стек може містити структуру проєкту, імена файлів, SQL-запити та інші внутрішні дані. Клієнту слід повертати загальну помилку.
response.end()Після завершення відповіді заголовки вже відправлені. Усі заголовки потрібно встановити до response.end().
Додавайте заголовки безпеки централізовано для всіх відповідей API.
Використовуйте сувору CSP для endpoint, які не повинні завантажувати ресурси.
Встановлюйте X-Content-Type-Options: nosniff разом із коректним Content-Type.
Увімкніть HSTS лише для повністю налаштованого HTTPS.
Забороняйте вбудовування через X-Frame-Options і frame-ancestors.
Для приватних відповідей використовуйте Cache-Control: no-store.
Не розкривайте стек помилки, версії та службові деталі клієнту.
Не довіряйте X-Forwarded-Proto без налаштованого довіреного proxy.