Пошук уроків, статей та іншого контенту
Налаштуємо Cross-Origin Resource Sharing і розберемо preflight-запити та безпечну політику доступу.
CORS (Cross-Origin Resource Sharing) — механізм браузера, який контролює доступ JavaScript-коду до ресурсів з іншого origin.
Origin складається з трьох частин:
протоколу;
домену;
порту.
Наприклад, ці адреси мають різні origin:
http://localhost:3000
http://localhost:4000
https://localhost:3000
http://example.com
Якщо frontend працює на http://localhost:3000, а API — на http://localhost:4000, браузер вважає такий запит cross-origin.
За замовчуванням браузер застосовує політику Same-Origin Policy. Вона забороняє JavaScript читати відповіді від іншого origin, якщо сервер явно не дозволив це через CORS-заголовки.
CORS — це обмеження браузера, а не механізм автентифікації. Запит через
curl, Postman або серверний код не блокується політикою браузера.
Сервер повідомляє браузеру дозволену політику за допомогою HTTP-заголовків.
Access-Control-Allow-OriginВказує origin, якому дозволено читати відповідь:
Access-Control-Allow-Origin: http://localhost:3000Можна вказати *:
Access-Control-Allow-Origin: *Але * не можна використовувати разом із credentials — наприклад, cookie або HTTP-авторизацією, яку браузер надсилає як credentialed request.
Небезпечний варіант:
Access-Control-Allow-Origin: *для приватного API. Він дозволяє будь-якому origin читати публічну відповідь. Для API з обмеженим доступом краще використовувати явний список дозволених origin.
Access-Control-Allow-MethodsВказує дозволені HTTP-методи:
Access-Control-Allow-Methods: GET, POST, PUT, DELETEAccess-Control-Allow-HeadersВказує заголовки, які frontend може надсилати:
Access-Control-Allow-Headers: Content-Type, AuthorizationAccess-Control-Allow-CredentialsДозволяє браузеру виконувати credentialed cross-origin requests:
Access-Control-Allow-Credentials: trueДля таких запитів сервер повинен вказувати конкретний origin, а не *.
Access-Control-Expose-HeadersЗа замовчуванням JavaScript має обмежений доступ до заголовків відповіді. Цей заголовок відкриває додаткові заголовки:
Access-Control-Expose-Headers: X-Request-IdVary: OriginЯкщо значення Access-Control-Allow-Origin залежить від вхідного origin, потрібно додати:
Vary: OriginЦе повідомляє кешам, що відповіді для різних origin не можна бездумно змішувати.
CORS-запити умовно поділяються на прості та ті, яким потрібна попередня перевірка.
Браузер може одразу надіслати запит, якщо він відповідає обмеженим умовам. Наприклад:
метод GET, HEAD або POST;
використовуються дозволені заголовки;
для POST використовується простий тип вмісту:
application/x-www-form-urlencoded;
multipart/form-data;
text/plain.
Навіть для простого запиту браузер перевіряє заголовок Access-Control-Allow-Origin у відповіді.
Якщо запит потенційно може мати більший вплив, браузер спочатку надсилає OPTIONS-запит. Це називається preflight.
Наприклад, такий запит зазвичай викликає preflight:
fetch("http://localhost:4000/api/profile", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({ name: "Olena" })
});Причина — метод POST із заголовком Content-Type: application/json.
Preflight може виглядати так:
OPTIONS /api/profile HTTP/1.1
Origin: http://localhost:3000
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-typeСервер має відповісти, наприклад:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: http://localhost:3000
Access-Control-Allow-Methods: GET, POST
Access-Control-Allow-Headers: Content-TypeЯкщо відповідь не дозволяє потрібний метод або заголовки, браузер не виконає основний POST.
CORS можна налаштувати вручну за допомогою стандартного модуля node:http. Це допомагає зрозуміти, які саме запити та заголовки обробляються.
Нижче наведено повний приклад API:
const http = require("node:http");
const port = 4000;
const allowedOrigins = new Set([
"http://localhost:3000",
"http://localhost:5173"
]);
const allowedMethods = new Set(["GET", "POST"]);
const allowedHeaders = new Set([
"content-type",
"authorization"
]);
function sendJson(response, statusCode, data, corsOrigin) {
response.statusCode = statusCode;
response.setHeader("Content-Type", "application/json; charset=utf-8");
if (corsOrigin) {
response.setHeader("Access-Control-Allow-Origin", corsOrigin);
response.setHeader("Access-Control-Allow-Credentials", "true");
response.setHeader("Access-Control-Expose-Headers", "X-Request-Id");
response.setHeader("Vary", "Origin");
}
response.setHeader("X-Request-Id", "request-123");
response.end(JSON.stringify(data));
}
function handlePreflight(request, response, origin) {
const requestedMethod = request.headers["access-control-request-method"];
const requestedHeaders = (request.headers["access-control-request-headers"] || "")
.split(",")
.map((header) => header.trim().toLowerCase())
.filter(Boolean);
const methodAllowed =
requestedMethod && allowedMethods.has(requestedMethod);
const headersAllowed = requestedHeaders.every((header) =>
allowedHeaders.has(header)
);
if (!origin || !allowedOrigins.has(origin)) {
response.statusCode = 403;
response.end("Origin is not allowed");
return;
}
if (!methodAllowed || !headersAllowed) {
response.statusCode = 403;
response.end("CORS preflight is not allowed");
return;
}
response.statusCode = 204;
response.setHeader("Access-Control-Allow-Origin", origin);
response.setHeader("Access-Control-Allow-Credentials", "true");
response.setHeader(
"Access-Control-Allow-Methods",
[...allowedMethods].join(", ")
);
response.setHeader(
"Access-Control-Allow-Headers",
[...allowedHeaders].join(", ")
);
response.setHeader("Access-Control-Max-Age", "600");
response.setHeader("Vary", "Origin");
response.end();
}
const server = http.createServer((request, response) => {
const origin = request.headers.origin;
if (request.method === "OPTIONS") {
handlePreflight(request, response, origin);
return;
}
if (origin && !allowedOrigins.has(origin)) {
sendJson(
response,
403,
{ error: "Origin is not allowed" }
);
return;
}
if (request.method === "GET" && request.url === "/api/profile") {
sendJson(
response,
200,
{
id: 1,
name: "Olena"
},
origin
);
return;
}
if (request.method === "POST" && request.url === "/api/profile") {
let body = "";
request.setEncoding("utf8");
request.on("data", (chunk) => {
body += chunk;
});
request.on("end", () => {
let profile;
try {
profile = JSON.parse(body);
} catch {
sendJson(
response,
400,
{ error: "Request body must be valid JSON" },
origin
);
return;
}
sendJson(
response,
200,
{
message: "Profile updated",
profile
},
origin
);
});
return;
}
sendJson(
response,
404,
{ error: "Not found" },
origin
);
});
server.listen(port, () => {
console.log(`API is running at http://localhost:${port}`);
});Збережіть код у файл server.js і запустіть:
node server.jsЦей сервер:
дозволяє лише два origin;
обробляє OPTIONS для preflight;
дозволяє лише GET і POST;
дозволяє лише Content-Type та Authorization;
підтримує credentials;
додає Vary: Origin;
відхиляє невідомі origin.
Frontend на http://localhost:3000 може виконати такий запит:
async function updateProfile() {
const response = await fetch("http://localhost:4000/api/profile", {
method: "POST",
credentials: "include",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
name: "Iryna"
})
});
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
const data = await response.json();
console.log(data);
}
updateProfile().catch(console.error);Оскільки використано Content-Type: application/json, браузер спочатку відправить OPTIONS, а потім, якщо preflight успішний, — POST.
У DevTools браузера послідовність буде приблизно такою:
OPTIONS /api/profile;
POST /api/profile.
Якщо OPTIONS отримує помилку або в його відповіді немає потрібних CORS-заголовків, POST не буде виконано.
Якщо frontend повинен надсилати cookie, потрібно встановити credentials:
fetch("http://localhost:4000/api/profile", {
credentials: "include"
});Сервер у такому випадку повинен повернути:
Access-Control-Allow-Origin: http://localhost:3000
Access-Control-Allow-Credentials: trueЦя конфігурація неправильна:
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: trueБраузер заблокує таку відповідь. Для credentials завжди вказуйте конкретний origin.
Не слід також без перевірки копіювати значення Origin у Access-Control-Allow-Origin:
// Небезпечно: будь-який origin буде автоматично дозволено
response.setHeader("Access-Control-Allow-Origin", request.headers.origin);Спочатку origin потрібно порівняти зі списком дозволених значень.
CORS не захищає endpoint від прямого виклику:
curl http://localhost:4000/api/profilecurl не застосовує правила браузера. Зловмисник також може надсилати HTTP-запити власними інструментами.
Тому доступ до приватних даних потрібно захищати окремо:
перевіряти сесію або токен;
перевіряти права користувача;
не покладатися лише на заголовок Origin;
перевіряти вхідні дані.
CORS визначає, чи може код у браузері прочитати відповідь. Він не є системою контролю доступу до API.
Для production-конфігурації:
використовуйте список конкретних дозволених origin;
не використовуйте * для приватних API;
дозволяйте лише необхідні HTTP-методи;
дозволяйте лише необхідні заголовки;
не вмикайте credentials без потреби;
додавайте Vary: Origin, якщо origin вибирається динамічно;
не вважайте успішний preflight доказом автентичності користувача;
перевіряйте CORS-політику для кожного середовища окремо.
Список origin краще задавати конфігурацією середовища, а не безпосередньо в коді:
const allowedOrigins = new Set(
process.env.ALLOWED_ORIGINS
.split(",")
.map((origin) => origin.trim())
);Наприклад:
ALLOWED_ORIGINS=http://localhost:3000,https://app.example.com node server.jsFrontend надсилає:
Content-Type: application/jsonале сервер не вказує Content-Type у Access-Control-Allow-Headers. У результаті preflight завершується помилкою.
Розробник додає CORS-заголовки до POST, але не обробляє OPTIONS. Браузер зупиняється ще на preflight і не надсилає POST.
* разом із credentialsЦе несумісна конфігурація:
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: trueПотрібно вказати конкретний origin.
Якщо помилка 400, 401, 403 або 500 не містить CORS-заголовків, frontend може побачити лише загальну CORS-помилку замість коректної відповіді API.
Не варто дозволяти origin за допомогою часткового збігу:
origin.includes("example.com")Це може дозволити небажані значення. Порівнюйте повний рядок зі списком дозволених origin.
CORS контролює cross-origin доступ браузера до відповідей API.
Сервер дозволяє origin через Access-Control-Allow-Origin.
Preflight — це OPTIONS-запит перед основним запитом.
Для preflight потрібно обробити дозволений метод і заголовки.
Access-Control-Allow-Origin: * не можна використовувати з credentials.
Список origin має бути явним і обмеженим.
CORS не замінює автентифікацію та авторизацію.