CORS починається з same-origin policy
Same-origin policy є базовим механізмом безпеки браузера. Він обмежує можливість документа або скрипту читати дані з іншого origin і, зокрема, не дозволяє шкідливому сайту читати дані із сервісу, де користувач уже авторизований. [3]
CORS, Cross-Origin Resource Sharing, є HTTP-механізмом, який дозволяє серверу контрольовано послабити це правило та явно визначити origin, яким можна читати відповідь. Fetch Standard описує CORS як opt-in protocol. [1][2]
Що саме означає origin
| Сторінка | Цільова URL | Співвідношення | Чому |
|---|---|---|---|
| https://app.example.com | https://app.example.com/api | Той самий origin | Ті самі schema, host і port |
| https://app.example.com | https://api.example.com | Інший origin | Інший host |
| https://app.example.com | http://app.example.com | Інший origin | Інша schema |
| https://app.example.com | https://app.example.com:8443 | Інший origin | Інший port |
| https://app.example.com | https://app.example.com/v2 | Той самий origin | Змінюється лише path |
Origin визначається схемою, host і port. Дві URL є same-origin лише тоді, коли збігаються всі три значення. Path не має значення. [3][4]
`https://app.example.com` і `https://api.example.com` тому є різними origin. Так само HTTP проти HTTPS або різні ports. [4]
Same-origin і same-site не є однаковими поняттями. Це особливо важливо для cookies, бо `SameSite` використовує поняття site, а CORS працює з origin. [13]
Що насправді означає браузер блокує API
Фраза про те, що браузер блокує request, часто надто спрощує ситуацію. Для простого cross-origin request браузер може надіслати request, отримати коректну HTTP-відповідь, а потім не віддати її JavaScript, якщо CORS check не пройдено. [1][6]
Для requests із preflight послідовність інша. Спочатку надсилається `OPTIONS`. Якщо preflight не пройдено, основний request не надсилається. [2][5][6]
Ця різниця важлива для операцій, що змінюють дані. CORS error не означає автоматично, що backend не отримав жодного request.
Прості requests: коли preflight не потрібен
| Method | Приклад headers | Простий request | Результат |
|---|---|---|---|
| GET | Accept | Так | Без preflight |
| POST | Content-Type: text/plain | Так | Без preflight |
| POST | Content-Type: application/json | Ні | JSON запускає preflight |
| PUT | Content-Type: application/json | Ні | Method запускає preflight |
| GET | Authorization: Bearer ... | Ні | Header запускає preflight |
Request є простим лише тоді, коли відповідає CORS safelist. Дозволені methods: `GET`, `HEAD` і `POST`. Headers, встановлені вручну, мають бути safelisted, а `Content-Type` може бути лише `application/x-www-form-urlencoded`, `multipart/form-data` або `text/plain`. [1]
Звичайний `POST` із `Content-Type: application/json` не є простим і зазвичай запускає preflight. Те саме стосується `PUT`, `DELETE` або custom header на кшталт `X-Request-ID`. [1][11]
Preflight OPTIONS: що перевіряє браузер
Preflight є автоматичним `OPTIONS` request. Браузер надсилає `Origin`, `Access-Control-Request-Method` і за потреби `Access-Control-Request-Headers`. Сервер відповідає дозволеними origin, methods і headers. [2][5]
Якщо застосунок хоче надіслати `DELETE` з `Authorization`, браузер може спочатку перевірити, чи дозволено цьому origin використовувати такий method і header. Лише позитивна відповідь дозволяє основний request. [5]
Preflight додає ще один network round trip, але його результат можна зберігати в окремому CORS preflight cache, незалежному від звичайного HTTP cache. [5][10]
Найважливіші CORS headers
| Header | Напрям | Призначення |
|---|---|---|
| Origin | Request | Origin, що ініціює request |
| Access-Control-Allow-Origin | Response | Origin, коду якого можна віддати response |
| Access-Control-Allow-Methods | Preflight response | Methods, дозволені preflight |
| Access-Control-Allow-Headers | Preflight response | Request headers, дозволені preflight |
| Access-Control-Allow-Credentials | Response | Дозвіл передати response із credentials |
| Access-Control-Expose-Headers | Response | Додаткові response headers, доступні JavaScript |
| Access-Control-Max-Age | Preflight response | Тривалість cache результату preflight |
| Vary: Origin | Response | Повідомляє cache, що response залежить від Origin |
`Origin` є request header, що вказує origin, який ініціює request. `Access-Control-Allow-Origin` є server response header, який визначає, чи може браузер передати відповідь коду з цього origin. [1][2][8]
`Access-Control-Allow-Methods` і `Access-Control-Allow-Headers` особливо важливі для preflight. `Access-Control-Allow-Credentials` дозволяє передати відповідь із credentials, а `Access-Control-Expose-Headers` робить додаткові response headers доступними JavaScript. [1][2]
Якщо сервер динамічно змінює `Access-Control-Allow-Origin`, MDN рекомендує також `Vary: Origin`. [1]
`Access-Control-Allow-Origin`: wildcard чи конкретний origin
`Access-Control-Allow-Origin: *` дозволяє ділитися відповіддю з будь-яким origin для requests без credentials. Для справді публічного API це може бути правильно. [2][8]
Для приватних або чутливих endpoints набір дозволених origin має бути мінімальним. MDN і OWASP рекомендують конкретні origin, якщо глобальний доступ не потрібен. [14][17]
Якщо дозволено кілька origin, сервер має порівнювати вхідний `Origin` з allowlist і повертати лише перевірене значення. Сліпе віддзеркалення будь-якого origin є небезпечним. [8][16]
Cookies і credentials: часте джерело проблем
Fetch може використовувати `credentials: "include"` для credentials у cross-origin requests. Цього недостатньо. Сервер має повернути `Access-Control-Allow-Credentials: true` і конкретний `Access-Control-Allow-Origin`. `*` у такому випадку не дозволений. [2][6][9]
Cookies також мають власні правила. `SameSite` може не дозволити cookie надсилатися в cross-site request, а `SameSite=None` вимагає `Secure`. [13]
CORS і `SameSite` вирішують різні проблеми. CORS може бути налаштований правильно, але cookie не надсилається, або cookie може надсилатися, а CORS все одно блокує доступ JavaScript до відповіді.
Чому `mode: "no-cors"` зазвичай нічого не виправляє
`fetch(..., { mode: "no-cors" })` не обходить CORS, якщо застосунок має читати API response. Methods і headers обмежуються, а відповідь є opaque. JavaScript не може читати body або headers, а видимий status дорівнює `0`. [6][7][11]
`no-cors` має спеціалізовані сценарії, зокрема деякі випадки Service Worker, але для звичайного JSON API це зазвичай неправильне рішення. [6]
Якщо зовнішнє API не підтримує CORS, власний backend або proxy може виконати server-to-server request. [11][12]
Чому curl, Postman або backend можуть працювати
Same-origin policy і CORS забезпечуються браузером. HTTP-клієнт поза цією моделлю не проходить такий самий CORS check. Тому endpoint може працювати в curl або API tool, але блокуватися для JavaScript на сторінці. [2][3][16]
Це також показує, чому CORS не є authentication для API. Клієнт поза браузером може встановити власний `Origin`, тому OWASP не радить використовувати цей header як доказ identity. [16]
API все одно потребує authorization, tokens, sessions, permission checks і server-side validation.
CORS не замінює CSRF protection або authorization
Same-origin policy переважно обмежує cross-origin reads. Forms і деякі прості requests все одно можуть надсилатися cross-origin, тому застосунки на cookies потребують CSRF protection. [3][15]
OWASP прямо рекомендує не покладатися лише на CORS або `Origin` для access control до чутливих resources. Authentication і authorization потрібні незалежно від CORS. [16]
CORS відповідає на питання, чи може браузер віддати response коду з певного origin. Він не визначає, чи має користувач або клієнт право виконувати операцію.
Типові помилки та діагностика
Відсутній `Access-Control-Allow-Origin` є однією з найпоширеніших помилок. Інші причини: заборонений method, відсутній header у `Access-Control-Allow-Headers`, `*` з credentials, неправильна OPTIONS-відповідь або redirects у CORS flow. [11][12]
JavaScript навмисно отримує мало деталей. Console і Network panel у DevTools корисніші для точної діагностики. [1][11]
`CORS request did not succeed` також може бути спричинений DNS, timeout, connection refused, TLS, mixed content або browser extensions. [11]
- Перевірте точний frontend origin: schema, host і port.
- Перевірте `OPTIONS`, якщо він є.
- Перевірте status і `Access-Control-Allow-*` у preflight response.
- Перевірте, що основна response також має потрібні CORS headers.
- Для cookies перевірте `credentials`, `Access-Control-Allow-Credentials`, `SameSite` і `Secure`.
- Перевірте redirects, TLS, mixed content і network errors.
- Порівняйте browser request із робочим curl або API-tool request.
Небезпечні CORS-конфігурації
Сліпе повернення вхідного `Origin` небезпечне, якщо requests із credentials повертають чутливі дані. Зловмисник може контролювати власний origin і намагатися читати responses у контексті авторизованого користувача. [16]
Надто широкі regular expressions і довіра всім subdomains також ризиковані, якщо одну з них можна захопити. OWASP рекомендує точний allowlist. [16][17]
`Access-Control-Allow-Origin: *` не є автоматичною вразливістю для повністю публічних даних без credentials. Проблемою є політика, ширша за реальну потребу. [8][17]
Продуктивність: вартість preflight і `Access-Control-Max-Age`
Preflight додає додатковий `OPTIONS` перед основним request і може збільшити latency. Його результат можна cache за допомогою `Access-Control-Max-Age` в окремому preflight cache. [5][10]
MDN вказує default 5 секунд без header. Браузери мають власні максимуми. Firefox обмежує до 86400 секунд, Chromium від версії 76 до 7200 секунд. [10]
Стабільні policies варто cache, але не настільки довго, щоб помилково надто широку політику було важко швидко відкликати.
Практична конфігурація та checklist deployment
Спочатку вирішіть, чи endpoint взагалі має бути доступним cross-origin. Якщо ні, CORS headers не потрібні. Якщо так, визначте allowlist origin, потрібні methods і headers та необхідність credentials. [14][17]
Для dynamic allowlist повертайте лише перевірений origin і додавайте `Vary: Origin`. З credentials використовуйте конкретний origin та `Access-Control-Allow-Credentials: true`. Frontend не може замінити це рішення сервера. [1][2]
Після deployment протестуйте простий request, preflight, сценарій з cookie, заборонений origin і реальні errors у DevTools.
- Чи справді потрібен cross-origin доступ?
- Які точні origin мають бути дозволені?
- Чи потрібні credentials?
- Які methods і request headers потрібні?
- Чи доходить `OPTIONS` до застосунку і чи відповідає коректно?
- Чи має основна response правильні CORS headers?
- Чи містять dynamic responses `Vary: Origin`?
- Чи endpoint все ще вимагає звичайні authentication і authorization?
- Чи протестована політика з дозволеним і забороненим origin?

