Перейти к содержимому

CORS (Cross-Origin Resource Sharing)

CORS — механизм браузера, позволяющий серверу разрешать cross-origin запросы через HTTP-заголовки; это не защита сервера от запросов, а контроль того, какие ответы браузер передаёт скрипту — понимание этого отличает junior от senior и тема обязательно поднимается при разборе fetch-ошибок.

CORS Preflight Flow

Browser  origin: https://app.example.com
      |
|  1. OPTIONS /api/data          <- Preflight (перед «опасным» запросом)
|     Origin: https://app.example.com&#10;      |     Access-Control-Request-Method:  POST
|     Access-Control-Request-Headers: Content-Type, X-Custom-Header
|
v
Server   api.example.com
|  2. 204 No Content
|     Access-Control-Allow-Origin:  https://app.example.com&#10;      |     Access-Control-Allow-Methods: GET, POST, DELETE
|     Access-Control-Allow-Headers: Content-Type, X-Custom-Header
|     Access-Control-Max-Age:       86400   (кэш preflight, сек)
|
v
Browser — preflight OK -> отправляет настоящий запрос
|
|  3. POST /api/data
|     Origin: https://app.example.com&#10;      |     Content-Type: application/json
|
v
Server
|  4. 200 OK + Access-Control-Allow-Origin: https://app.example.com&#10;      v
Browser — заголовок совпадает -> передаёт ответ скрипту

Simple requests (GET/POST + safe headers + form CT) — preflight НЕ нужен!
Триггеры preflight: PUT/PATCH/DELETE, кастомные заголовки, JSON Content-Type
// Server-side CORS middleware (Node.js / Express)
app.use((req, res, next) =&gt; {
const ALLOWED = new Set([
'https://app.example.com',
'https://staging.example.com',
]);
const origin = req.headers.origin;
if (ALLOWED.has(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin); // конкретный origin, не '*'
res.setHeader('Vary', 'Origin'); // кэш должен учитывать Origin
}
res.setHeader('Access-Control-Allow-Methods', 'GET,POST,PUT,PATCH,DELETE,OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type,Authorization,X-Request-Id');
res.setHeader('Access-Control-Max-Age', '86400'); // preflight кэш на 24 ч
if (req.method === 'OPTIONS') return res.sendStatus(204);
next();
});
// Exposed headers: по умолчанию JS видит только «safe» заголовки ответа
res.setHeader('Access-Control-Expose-Headers', 'X-RateLimit-Remaining, X-Request-Id');
// Теперь в браузере: response.headers.get('X-RateLimit-Remaining') !== null
// Client: credentials и ограничения wildcard
fetch('https://api.example.com/data', {
credentials: 'include', // отправляет cookies cross-origin
});
// Сервер ОБЯЗАН ответить конкретным origin (не '*') + credentials:
// Access-Control-Allow-Origin: https://app.example.com (НЕ *)
// Access-Control-Allow-Credentials: true
// Проверка: CORS не защищает сервер!
// curl -H 'Origin: https://evil.com' https://api.example.com/data
// → сервер всё равно обработает запрос; CORS блокирует только доступ к ответу в браузере.
// Для защиты данных нужна авторизация на сервере.
// CORS и preflight-кэш
// Access-Control-Max-Age: 86400 → браузер не будет делать preflight 24 ч
// Максимум в Chrome: 7200 с (2 ч), Firefox: 86400 с — зависит от браузера
// 0 → отключить кэш (принудительный preflight на каждый запрос)

Итог: CORS проверяется браузером, управляется заголовками сервера; preflight нужен для non-simple запросов; credentials требуют явного разрешения с обеих сторон.