vyvchy
    Теми розділу

    03 · Веб і мережі для AQA

    CORS і політика одного походження

    Зміст

    Класична сцена на дейлі: фронтендер каже «CORS-помилка, винен бекенд», бекендер відкриває Postman, показує зелений 200 і відповідає «у мене все віддається», а тестувальник має розсудити, хто правий. Розсудити можна за півхвилини, якщо тримати в голові одну річ: заголовки Access-Control-* — це інструкція для браузера, а не для сервера. Postman їх не читає взагалі, тож його зелений статус не є аргументом ні в чий бік.

    Це канонічна глава теми: тут повний виклад , й CORS, решта глав розділу посилаються сюди. Ціль — щоб із «якийсь CORS» у тебе виходив конкретний баг-репорт: який Origin пішов, якого заголовка не повернулося, чи був OPTIONS і хто це лагодить.

    Походження: схема, хост, порт — і нічого більше

    Походження (origin) — це трійка «схема (scheme) + хост (host) + порт (port)». Дві адреси належать одному походженню тоді й лише тоді, коли збігаються всі три складові; шлях, параметри запиту й фрагмент у неї не входять.

    Складова https://app.example.com:443/users?id=5#topЗначенняВ origin?
    Схемаhttpsтак
    Хостapp.example.comтак
    Порт443так
    Шлях, запит, фрагмент/users, ?id=5, #topні

    Це не деталь адресації, а межа безпеки: RFC 6454 називає походження доменом захисту (protection domain) і дозволяє доступ до обʼєктів — DOM, браузерних API — тоді й лише тоді, коли адреси належать одному походженню.

    Дві речі, на яких спотикаються найчастіше. Піддомен — це інший хост: app.example.com і api.example.com — різні походження, попри спільний домен другого рівня. Порт залежить від схеми: «особливі» схеми ftp, http, https, ws, wss мають типовий порт, який у серіалізації опускають — для http це 80, для https 443, — тому https://site.com і https://site.com:443 — одне походження, а https://site.com:8443 — уже інше. Звідси й головна пастка локального стенду: фронт на http://localhost:3000 і API на http://localhost:4000 — це два різні походження з усіма правилами. Структура адреси розібрана в главі «URL і кодування», порт як мережеве поняття — у главі «DNS, IP, порти та мережа».

    Специфікація обчислює походження switch-ом за схемою, і кортеж повертають лише пʼять схем — ftp, http, https, ws, wss, — причому кортеж чотиричленний: схема, хост, порт, домен. Для file кортежного походження немає взагалі: «When in doubt, return a new opaque origin». Непрозоре (opaque) походження — окремий вид, а не «порожня» трійка: єдина осмислена операція над ним — перевірка на рівність, серіалізується воно рядком null, а data:-URL не є same-origin навіть сам із собою. Звідси й те, що тест, відкритий як локальний file:///, поводиться інакше, ніж на стенді.

    Рядок null браузер шле і як значення заголовка Origin — зокрема зі сторінок пісочного (sandboxed) <iframe> без allow-same-origin: «forces content into an opaque origin». Він же «prevents script from reading from or writing to the document.cookie IDL attribute, and blocks access to localStorage» — тобто «кукі не ставляться всередині фрейма» в пісочниці є очікуваною поведінкою, а не багом.

    Походження — не те саме, що сайт

    Ціна плутанини висока, бо кукі рахують сайт, а політика одного походження — походження. «Сайт» у HTML Standard — це пара «схема + хост», де хост згорнуто до (registrable domain — плюс мітка ліворуч від нього); порт і домен у сайтових перевірках ігноруються взагалі. Тому app.example.com і api.example.com — різні походження, але один сайт: те, що для SOP чужина, для кукі своє. А от http://example.com і https://example.com — не той самий сайт: схема входить у критерій. Сама специфікація радить уникати сайтових перевірок на користь звірки за походженням, бо поняття публічного суфікса й реєстрованого домену «cannot be relied-upon to provide a hard security boundary». Докладніше про кукі — у главі «Кукі, сесії та сховище браузера».

    Що політика одного походження забороняє, а що ні

    Політика одного походження (same-origin policy, SOP) — вбудований механізм безпеки браузера, який обмежує, як документ або скрипт з одного походження може взаємодіяти з ресурсом іншого. Це не заголовок і не налаштування: вона діє завжди й за замовчуванням. Мета, яку називає MDN, — ізолювати потенційно шкідливі документи, щоб чужий сайт не прочитав дані сервісу, у якому користувач авторизований (пошта, інтранет, банк), і не передав їх .

    Ключова асиметрія в тому, що політика ділить дії на три класи, а не на «можна/не можна»:

    Клас діїПрикладиДозволено?
    Записи (writes)посилання, редиректи, надсилання формизазвичай так
    Вбудовування (embedding)<script src>, <link rel="stylesheet">, <img>, <video>, вміст <iframe>зазвичай так
    Читання (reads)доступ до відповіді, до DOM чужого документазазвичай ні

    Звідси відповідь на класичне питання «чому <img> і <script> не блокуються»: вбудувати чуже можна, витягнути його вміст назад у скрипт — ні. Картинку браузер намалює, але щойно ти малюєш у <canvas> дані з іншого походження без CORS-дозволу, canvas стає «забрудненим» (tainted), і поіменно ламаються getImageData() на контексті та toBlob(), toDataURL(), captureStream() на самому елементі — з помилкою SecurityError. Знімається це конфігурацією сервера зображень, який має віддавати Access-Control-Allow-Origin, а не з боку клієнта.

    Політика при цьому не абсолютна: сам факт вбудовування все одно щось повідомляє — розміри картинки, доступність ресурсу. Заборонити вбудовування свого документа сайт може заголовком X-Frame-Options. Посилання на вікно чужого походження (iframe.contentWindow, window.parent, window.opener) дають лише дуже обмежений доступ до Window і Location; штатний канал спілкування між документами різного походження — window.postMessage. localStorage і sessionStorage теж ізольовані за походженням, тож прочитати чуже сховище скриптом не вийде; у кукі область своя — домен і шлях, — тому їхні межі з походженням не збігаються.

    Наша практика (не канон). Історичним обходом цієї заборони був JSONP: сервер повертав не чистий JSON, а виклик функції, і його підключали через <script>. Сьогодні прийом вважають застарілим і небезпечним, але саме він пояснює, навіщо взагалі знадобився CORS; окремого канонічного джерела під цей сюжет у нас немає.

    CORS: дозвіл видає сервер, застосовує браузер

    Чистий SOP занадто суворий для реального життя: застосунок на https://app.example.com майже завжди ходить по дані на https://api.example.com. CORS (Cross-Origin Resource Sharing) — механізм на основі HTTP-заголовків, яким сервер повідомляє, з яких чужих походжень браузеру дозволено вантажити його ресурси й читати відповіді. Він не «вимикає» безпеку, а дає серверу контрольований спосіб її послабити.

    Чому механізм узагалі влаштований як opt-in, каже сама специфікація Fetch: інакше течуть дані з-за фаєрвола (інтранет), а разом з обліковими даними — ще й чутливі; поєднання «ділитися відповідями + дозволити облікові дані» вона називає доволі небезпечним і вказує клас confused deputy.

    Механіка простого випадку: скрипт робить запит на інше походження, і браузер сам додає заголовок Origin.

    GET /users HTTP/1.1
    Host: api.example.com
    Origin: https://app.example.com

    Сервер підтверджує дозвіл заголовком відповіді:

    HTTP/1.1 200 OK
    Content-Type: application/json
    Access-Control-Allow-Origin: https://app.example.com
    API (api.example.com)БраузерСкрипт (app.example.com)API (api.example.com)БраузерСкрипт (app.example.com)Звірка Origin із дозволомfetch на інше походженняGET /users + Origin200 + Access-Control-Allow-OriginВіддає відповідь скриптуAPI (api.example.com)БраузерСкрипт (app.example.com)API (api.example.com)БраузерСкрипт (app.example.com)Звірка Origin із дозволомfetch на інше походженняGET /users + Origin200 + Access-Control-Allow-OriginВіддає відповідь скрипту

    Браузер звіряє походження сторінки з тим, що дозволив сервер, і лише тоді віддає відповідь скрипту; якщо заголовка немає взагалі або значення інше — читання заблоковано. Access-Control-Allow-Origin називає одне дозволене походження або *, і зірочка припустима лише для запитів без облікових даних. Оскільки одне значення покриває один origin, сервери зазвичай читають вхідний Origin, звіряють його зі списком дозволених і лише за збігу віддають те саме значення назад. Пропущений крок звірки — це вже : віддзеркалення будь-якого Origin відкриває дані кому завгодно. І при віддзеркаленні відповідь мусить нести Vary: Origin, інакше кеш ( чи CDN) віддасть чужому походженню відповідь, призначену іншому — механіка кешу розібрана в главі «Кешування». Окремий дефект — Access-Control-Allow-Origin: null: створити документ із походженням null може будь-хто, тож джерело радить цього значення уникати.

    Навіть коли браузер віддав тіло відповіді, скрипту типово видно лише сім «безпечних» заголовків: Cache-Control, Content-Language, Content-Length, Content-Type, Expires, Last-Modified, Pragma. Решту сервер має явно перелічити в Access-Control-Expose-Headers. Це прямий гачок для тестів: якщо перевірка читає X-Total-Count з крос-оріджин відповіді й бачить null, причина зазвичай не в бекенді, який заголовок таки шле, а у відсутньому Access-Control-Expose-Headers.

    Простий запит і той, що вимагає попереднього дозволу

    Частину крос-оріджин запитів браузер шле одразу, а перед рештою питає в сервера дозволу окремим запитом. Термін «простий запит» варто вживати з обмовкою: MDN називає його спадком застарілої специфікації CORS — чинна Fetch, яка тепер CORS і визначає, слова simple request не вживає. Але поняття живе в обігу й на співбесіді його розуміють.

    Запит іде без попереднього дозволу, якщо виконано всі умови:

    УмоваЩо дозволено
    МетодGET, HEAD, POST
    Заголовки, виставлені вручнулише safelisted: Accept, Accept-Language, Content-Language, Content-Type, Range
    Значення Content-Typeapplication/x-www-form-urlencoded, multipart/form-data, text/plain
    Довжина значення заголовкане більша за 128 (інакше заголовок випадає з safelist)

    На практиці майже будь-який реальний API-запит із цих рамок виходить: він шле JSON із Content-Type: application/json, додає Authorization: Bearer … або використовує метод PUT, PATCH чи DELETE. Саме тому метод — найпомітніший тригер: GET летить сам, PATCH — ні. Семантика методів розібрана в главі «HTTP: методи, структура, заголовки».

    Preflight (попередній запит) — окремий CORS-запит методом OPTIONS, який браузер надсилає сам, до «справжнього» запиту. У ньому він заявляє наміри заголовками Access-Control-Request-Method і Access-Control-Request-Headers, а сервер відповідає переліком дозволеного:

    OPTIONS /users HTTP/1.1
    Origin: https://app.example.com
    Access-Control-Request-Method: POST
    Access-Control-Request-Headers: authorization, content-type
    
    HTTP/1.1 204 No Content
    Access-Control-Allow-Origin: https://app.example.com
    Access-Control-Allow-Methods: GET, POST, OPTIONS
    Access-Control-Allow-Headers: authorization, content-type
    Access-Control-Max-Age: 600

    Ні

    Так

    Ні

    Так

    Ні

    Так

    Так

    Ні

    Крос-оріджин запит

    GET / HEAD / POST?

    Preflight OPTIONS

    Лише safelisted заголовки?

    Content-Type із трьох дозволених?

    Летить одразу

    Дозвіл отримано?

    Летить справжній запит

    Мережева помилка, справжній запит не летить

    Ні

    Так

    Ні

    Так

    Ні

    Так

    Так

    Ні

    Крос-оріджин запит

    GET / HEAD / POST?

    Preflight OPTIONS

    Лише safelisted заголовки?

    Content-Type із трьох дозволених?

    Летить одразу

    Дозвіл отримано?

    Летить справжній запит

    Мережева помилка, справжній запит не летить

    Три деталі, які кусаються найчастіше. Успішним preflight вважається лише «ok»-статус — «restricted to an ok status, e.g., 200 or 204»: якщо OPTIONS віддає 401, 404 чи редирект, справжній запит не полетить зовсім. Preflight ніколи не йде з обліковими даними, тому вішати на маршрут OPTIONS перевірку авторизації не можна — вона його завалить. І Access-Control-Max-Age каже, скільки секунд кешувати результат перевірки; типове значення за Fetch — 5 секунд, тож без явного заголовка OPTIONS повторюватиметься практично на кожен запит.

    Для тестів звідси випливає найпоширеніша плутанина: один логічний запит дає два записи в мережі — спершу OPTIONS, потім основний. Тому і мок, і очікування описують парою «метод + URL»: сама лише адреса тепер покриває два різні звернення.

    Облікові дані: чому зірочка з ними не працює

    За замовчуванням крос-оріджин fetch і XMLHttpRequest не шлють кукі й HTTP-автентифікацію. Режим облікових даних має три значення — omit, same-origin (типове) та include; до самих облікових даних належать кукі, клієнтські TLS-сертифікати й заголовки Authorization/Proxy-Authorization.

    // без цієї опції кукі на інше походження не підуть
    await fetch('https://api.example.com/me', { credentials: 'include' });

    Щойно запит іде з обліковими даними, вимоги до сервера жорсткішають:

    Заголовок відповідіБез облікових данихЗ обліковими даними
    Access-Control-Allow-Originможна *лише конкретне походження
    Access-Control-Allow-Credentialsне потрібенобовʼязково true
    Access-Control-Allow-Headers / -Methods / Expose-Headers* розкривається* не діє як «усі»

    Логіка проста: якщо ти віддаєш приватні дані під конкретну сесію, ти маєш назвати конкретне довірене походження, а не «будь-кого». Тому пара «фронт із credentials: 'include' + сервер із Access-Control-Allow-Origin: *» дає блокування, попри те що зірочка нібито дозволяє всім.

    Один виняток ламає Bearer-запити найчастіше, і він безумовний — тобто не залежить від режиму облікових даних. Fetch означує: «A CORS non- request-header name is a header name that is a byte-case-insensitive match for Authorization», а в алгоритмі preflight: якщо таке імʼя є серед заголовків запиту й не перелічене поіменно в Access-Control-Allow-Headers, повертається мережева помилка. Отже сервер із Access-Control-Allow-Headers: * пропустить будь-що, крім Authorization. Другий сюрприз того самого роду: при редиректі на інше походження браузер знімає Authorization — «the moment another origin is seen after the initial request, the Authorization header is removed», тож тест, який іде за крос-оріджин редиректом, отримає неавторизовану відповідь, і сервер тут ні до чого.

    Кукі в крос-оріджин контексті мають власний, незалежний рубіж — атрибут SameSite. Strict шле кукі лише для запитів із того самого сайту, Lax додатково пропускає верхньорівневу навігацію , None дозволяє міжсайтові запити, але обовʼязково разом із Secure. Механізми перетинаються, але один одного не заміняють: credentials: 'include' дозволяє браузеру спробувати надіслати кукі, а SameSite вирішує, чи вона взагалі поїде. Тому «розлогінювання посеред E2E-сценарію» буває і недоналаштованим Access-Control-Allow-Credentials, і SameSite без None; Secure.

    CSRF: сусідня тема з іншим механізмом

    Плутати CORS і CSRF не можна, і різниця тримається рівно на асиметрії з розділу про SOP. Крос-оріджин записи дозволені — саме з цього дозволу, як прямо каже RFC 6454, і виростає міжсайтова підробка запиту (cross-site request forgery, CSRF): чужий сайт може націлити форму чи <img> на твоє походження, браузер сам підставить кукі, і дія станеться. Відповіді атакувальник не прочитає — і йому вона не потрібна.

    Тобто CORS — про читання відповіді, CSRF — про небажану дію. З цього випливає найважливіший практичний висновок: правильно налаштований CORS від CSRF не захищає, а «CORS заблокував» не означає «сервер нічого не зробив». Гайд тестування додає сюди ще одну звичну ілюзію: POST захистом не є, бо POST-запити автоматизуються через JavaScript, тож саме лише вживання POST вразливості не усуває.

    Захист від CSRF — окрема тема з власним механізмом: синхронізований токен, SameSite, звірка Origin/Referer. Її канон живе в розділі про безпеку; тут важливо лише, що це інший рубіж, і наявність одного нічого не каже про наявність другого. Практична сторона токена очима автотесту розібрана в главі «Автентифікація та авторизація».

    Інші межі: iframe і Shadow DOM

    Походження — не єдина межа, крізь яку «не проходить» пошук елемента, і плутають ці межі постійно.

    iframe — це не «ще один шар» сторінки, а окремий документ зі своїм window. Якщо він того самого походження, DOM усередині доступний, просто контекст треба перемкнути. Якщо чужого — політика одного походження лишає лише дуже обмежений доступ до Window і Location, тож до сторонніх платіжних форм і віджетів підтримки скрипт зі сторінки через DOM не дотягнеться: лишається postMessage або перевірка на рівні мережі. Драйвери при цьому працюють на рівні браузера, поза цим обмеженням: і frameLocator у Playwright, і switchTo().frame() у Selenium уміють перемкнути контекст у крос-оріджин фрейм.

    Shadow DOM — межа зовсім іншої природи: це інкапсуляція компонента, а не безпека. document.querySelectorAll() не бачить вузлів усередині , тому селектор для основного документа не пробиває. Playwright відкриті shadow tree пробивають самі, а XPath — ні. Режим closed теж не є барʼєром безпеки: MDN прямо каже, що це «more of an indication» і «there are ways it can be evaded». Механіка дерев і подій — у главі «DOM, селектори та події».

    Звідси порядок діагностики: симптом «у DevTools елемент видно, а локатор не знаходить» першим ділом перевіряють на інший контекст (iframe або Shadow DOM), і лише якщо фрейм чужий, це стає питанням походження.

    Чому «у Postman працює» нічого не доводить

    CORS перевіряє браузер. Postman, curl, HTTP-клієнт у бекенд-коді та APIRequestContext у Playwright політики одного походження не реалізують і заголовки Access-Control-* просто ігнорують: отримали відповідь — віддали її повністю. Тому зелений 200 в API-клієнті не є ані доказом, що з бекендом усе гаразд у , ані спростуванням баг-репорту.

    Специфікація формулює це прямо: «it is up to the client to determine and enforce the restriction of whether the client has access to the response data based on this header». Звідси два наслідки, які варто вимовляти вголос у суперечці. CORS не є серверним контролем доступу — будувати на ньому авторизацію не можна. І Origin підробний поза браузером: із JavaScript його змінити не можна, але покладатися на нього в перевірках доступу — погана ідея.

    Друга половина відповіді — що саме блокує браузер:

    Тип запитуЧи доходить до сервераЩо блокує браузер
    Без preflightтак, виконується повністюлише читання відповіді скриптом
    З preflightосновний — тільки після успішного OPTIONSі основний запит, і читання відповіді

    Тобто «CORS заблокував» ніколи не означає «запит не дійшов»: для запиту без preflight сервер його виконує, і всі побічні ефекти стаються. Для ізоляції стану між тестами це прямий ризик: прибирай дані явно, а не покладайся на «браузер же заблокував». І дзеркально: APIRequestContext чудово готує стан через API повз усі CORS-налаштування — але саме тому браузерну CORS-помилку ним не відтвориш, для неї потрібен реальний браузерний контекст.

    Наша практика (не канон). Далі — те, як розсуджують суперечку команди, з якими ми працювали; окремого стандарту під цей поділ немає. За замовчуванням CORS лагодять на бекенді: заголовки Access-Control-* віддає сервер, і фронтенд не може «домалювати» собі Access-Control-Allow-Origin. Винятків із боку фронта два — забутий режим із обліковими даними й зайвий , який без потреби тягне preflight. А замість «CORS не працює» у тікет ми пишемо факти: точний текст помилки з , значення Origin у запиті, наявні й відсутні Access-Control-* у відповіді та чи був OPTIONS. І перше, що звіряємо при незрозумілій мережевій помилці в E2E, — чи справді фронт і бек в очікуваному походженні: значна частина «працює на моїй машині, падає в CI» зводиться саме до різних origin між середовищами.

    Що видно у вкладці Network і як це мокати

    Крос-оріджин запити зазвичай народжуються з AJAX — обміну даними у фоні через fetch чи XMLHttpRequest, — тож і шукати їх треба у фільтрі Fetch/XHR. Кожен рядок вкладки Network — один запит; журнал пишеться, лише поки DevTools відкриті, тому панель відкривають перед відтворенням проблеми, а прапорець Preserve log рятує лог від редиректу після логіну.

    Колонка Status показує код або помилку CORS. Вкладка Headers розділяє General, Response Headers і Request Headers — саме там видно, який Origin пішов і чи повернувся Access-Control-Allow-Origin; щоб бачити цей заголовок у кожному рядку, його додають окремою колонкою через Response Headers > Manage Header Columns. Фільтр розуміє властивості на кшталт method:OPTIONS, і кілька властивостей поєднуються лише через AND — «OR operations aren't supported». Санітизований HAR (без Cookie, Set-Cookie й Authorization) безпечно чіпляти до тікета. Повний розбір панелі — у главі «DevTools: вкладка Network і дебаг».

    В автотестах головна пастка — мок, який забув про preflight. Мокаючи крос-оріджин відповідь, OPTIONS треба обробити окремо й повернути потрібні Access-Control-*, інакше справжній запит не піде:

    await page.route('**/api/orders', async (route) => {
      const headers = {
        'Access-Control-Allow-Origin': 'http://localhost:3000',
        'Access-Control-Allow-Methods': 'POST, OPTIONS',
        'Access-Control-Allow-Headers': 'authorization, content-type',
      };
      if (route.request().method() === 'OPTIONS') {
        return route.fulfill({ status: 204, headers });
      }
      await route.fulfill({ status: 201, headers, body: '{"id":1}' });
    });

    Ще одна деталь для перевірок: fetch не реджектиться на статусах 404 чи 500 — він реджектиться «on some errors, such as a network error», а невдала CORS-перевірка за специфікацією і є мережевою помилкою. Тому CORS-збій у коді виглядає як виняток, а не як відповідь із поганим статусом. Мокання й перехоплення глибше — у главі «Перехоплення й мокання мережі».

    Типові помилки

    • «У Postman 200, значить бекенд правий». Насправді Postman і curl політики одного походження не реалізують і заголовки Access-Control-* ігнорують — вони не можуть відтворити браузерну перевірку в принципі.
    • «CORS заблокував, отже запит не дійшов і дані не змінилися». Насправді запит без preflight сервер виконує повністю; браузер лише не віддає відповідь скрипту. Стан треба прибирати явно.
    • «Поставили Access-Control-Allow-Origin: * — тепер працює для всіх». Насправді з обліковими даними зірочка недійсна: браузер вимагає конкретне походження плюс Access-Control-Allow-Credentials: true.
    • «Access-Control-Allow-Headers: * пропускає будь-що». Насправді Authorization — CORS non-wildcard request-header name, і зірочка його не покриває ніколи, незалежно від облікових даних.
    • «Зайвий OPTIONS у Network — баг фронта». Насправді це preflight, який браузер шле сам; без явного Access-Control-Max-Age він повторюватиметься майже на кожен запит.
    • «localhost:3000 і localhost:4000 — один і той самий localhost». Насправді порт входить у трійку походження, тож це два різні походження з усіма крос-оріджин правилами.
    • «Заголовок сервер шле — тест його прочитає». Насправді скрипту типово видно лише сім safelisted-заголовків відповіді; кастомний треба перелічити в Access-Control-Expose-Headers.
    • «CORS налаштований — від CSRF ми захищені». Насправді це різні рубежі: CORS обмежує читання відповіді, CSRF експлуатує дозволений запис, для якого відповідь не потрібна.
    • «Локатор не знаходить елемент у чужому iframe — селектор поганий». Насправді це інший контекст: селектор основного документа межі фрейма не перетинає, тож драйвер треба явно перемкнути (frameLocator, switchTo().frame()). А от скрипт зі сторінки до DOM крос-оріджин фрейма справді не дотягнеться — це вже межа походження.

    Підсумок

    • Походження — це трійка «схема + хост + порт», і розбіжність хоч в одній складовій робить його іншим. Сайт — ширше поняття, і кукі рахують саме його.
    • Політика одного походження обмежує читання, а не звернення. Записи й вбудовування зазвичай дозволені — звідси і <img>/<script> без блокування, і CSRF як зворотний бік тієї самої дозволеності.
    • CORS — це дозвіл, який видає сервер, а застосовує браузер. Рішення «віддати скрипту дані чи ні» ухвалює браузер, тому клієнти без SOP тут нічого не доводять.
    • Preflight — окремий OPTIONS перед справжнім запитом, і в реальних API він летить майже завжди: JSON, Authorization, PUT/PATCH/DELETE. Успішним він вважається лише за ok-статусу й ніколи не йде з обліковими даними.
    • Облікові дані змінюють правила: * перестає діяти, потрібне конкретне походження й Access-Control-Allow-Credentials: true, а Authorization називають поіменно завжди.

    Можливі питання

    • «Що таке origin і що в нього входить?» Чекають трійку «схема + хост + порт» і чітке «шлях і параметри не входять». Сильна відповідь дає приклад із різними портами на локалхості й згадує, що піддомен — інший хост.
    • «Що саме забороняє same-origin policy?» Перевіряють, чи розуміє кандидат асиметрію. Слабка відповідь — «забороняє крос-оріджин запити»; сильна — «обмежує читання даних скриптом, а записи й вбудовування зазвичай дозволені», з прикладом <img> і забрудненого .
    • «Чому в Postman працює, а в браузері CORS-помилка?» Найчастіше питання теми. Відповідь: CORS застосовує браузер, а API-клієнти SOP не реалізують; заголовки Access-Control-* адресовані браузеру.
    • «Коли летить preflight і що це таке?» Дивляться на перелік тригерів: метод поза GET/HEAD/POST, заголовок поза переліком safelisted, Content-Type: application/json. Сильна відповідь додає, що OPTIONS мусить віддати ok-статус і йде без кукі.
    • «Чому Access-Control-Allow-Origin: * не працює з кукі?» Перевіряють розуміння режиму облікових даних: приватні дані під конкретну сесію вимагають конкретного походження плюс Access-Control-Allow-Credentials: true.
    • «CORS і CSRF — це одне й те саме?» Питання-пастка на межу тем. Відповідь: ні, CORS про читання відповіді, CSRF про небажану дію; правильний CORS від CSRF не рятує.
    • «Заблокований CORS-запит змінив дані на сервері?» Дивляться, чи знає кандидат різницю простого запиту й preflight: без preflight сервер виконує запит повністю, тож так, міг змінити.

    Джерела

    Походження: схема, хост, порт — і нічого більше

    Що політика одного походження забороняє, а що ні

    CORS: дозвіл видає сервер, застосовує браузер

    • MDN — Cross-Origin Resource Sharing (CORS) — CORS як механізм заголовків, Access-Control-Allow-Origin і його значення, Vary: Origin, safelisted-заголовки відповіді та Access-Control-Expose-Headers.
    • WHATWG Fetch Standard — нормативний алгоритм CORS-перевірки, підстава opt-in (витік даних інтранету, confused deputy), перелік із семи заголовків відповіді.
    • MDN — Access-Control-Allow-Origin — звірка Origin зі списком дозволених, вимога Vary: Origin, чому значення null треба уникати.

    Простий запит і той, що вимагає попереднього дозволу

    • MDN — Cross-Origin Resource Sharing (CORS) — «простий запит» як термін застарілої специфікації, умови без preflight, OPTIONS як preflight і його заголовки, Access-Control-Max-Age.
    • WHATWG Fetch Standard — перелік safelisted заголовків і Range, ліміт 128 на значення, дозволені значення Content-Type, вимога ok-статусу, відсутність облікових даних у preflight, типове значення Max-Age у 5 секунд.

    Облікові дані: чому зірочка з ними не працює

    • MDN — Cross-Origin Resource Sharing (CORS) — режим із обліковими даними, заборона * разом із ними та вимога Access-Control-Allow-Credentials: true.
    • WHATWG Fetch Standard — режими облікових даних, нерозкриття * у режимі з обліковими даними, Authorization як CORS non-wildcard request-header name і його зняття на крос-оріджин редиректі.
    • MDN — Using the Fetch API — склад облікових даних і три значення опції credentials із типовим same-origin.
    • MDN — Using HTTP cookies — значення SameSite і вимога Secure для None.

    CSRF: сусідня тема з іншим механізмом

    Інші межі: iframe і Shadow DOM

    • MDN — Same-origin policy — обмежений доступ до вікна чужого походження й postMessage як штатний канал.
    • MDN — Using shadow DOM — shadow tree як інкапсуляція, невидимість вузлів для querySelectorAll, closed як індикація, а не механізм безпеки.
    • Playwright — Locators — пробивання відкритих shadow tree локаторами й межа для XPath.
    • Playwright — Frames — перемикання контексту у фрейм.
    • Selenium — Working with IFrames and frames — фрейм як інший документ і потреба перемкнути контекст.

    Чому «у Postman працює» нічого не доводить

    Що видно у вкладці Network і як це мокати

    • Chrome DevTools — Network features reference — запис журналу лише при відкритих DevTools, Preserve log, колонки й вкладка Headers, власні колонки заголовків, синтаксис фільтра з AND, санітизований HAR.
    • MDN — Using the Fetch APIfetch не реджектиться на HTTP-помилках і реджектиться на мережевій.
    • WHATWG Fetch Standard — невдала CORS-перевірка як мережева помилка.
    • Playwright — Mock APIs — підміна відповіді через route/fulfill.
    • Playwright — Network — очікування конкретної відповіді за URL і методом.

    Пояснення

    «Поясни» працює з власним API-ключем Anthropic: запит іде з вашого браузера прямо до Anthropic.

    Свій API-ключ (BYOK)

    Вставте власний ключ Anthropic — пояснення працюватиме на реальній моделі. Ключ зберігається лише у вашому браузері (localStorage), нікуди не надсилається, крім api.anthropic.com, і не логується.