Прескочи към съдържанието
Урок 03Beginner

Same-Origin политика и CORS

Единствената истинска граница на сигурността в браузъра и заглавката, която нарочно пробива дупки в нея.

12 мин четенетест с 3 въпроса

Same-origin политиката е единствената смислена граница на сигурността в браузъра. Всичко друго — флагове, заглавки, токени — е усъвършенстване около нея. CORS е механизмът за нейното нарочно разхлабване, а зле конфигуриран CORS е една от най-честите находки при всяка истинска оценка на API.

Какво е origin

Origin е кортеж от три неща:

схема :// хост : порт

Два URL адреса са same-origin само ако и трите съвпадат точно. Без частични точки.

URL AURL BSame origin?
https://app.example.com/ahttps://app.example.com/bДа — пътят е без значение
https://app.example.comhttp://app.example.comНе — схемата се различава
https://app.example.comhttps://api.example.comНе — хостът се различава
https://app.example.comhttps://app.example.com:8443Не — портът се различава
https://example.comhttps://www.example.comНе — хостът се различава

Два капана тук. Първо, споделен родителски домейн не значи нищо за same-origin политиката, макар да значи много за бисквитките — бисквитка Domain=example.com се споделя между поддомейни, които не са same-origin помежду си. Това несъответствие е мястото, където живеят ескалациите чрез превземане на поддомейн. Второ, портът по подразбиране е част от кортежа дори когато не е изписан: https://example.com е порт 443, затова се различава от https://example.com:8443.

Как работи

Same-origin политиката не е едно правило. Тя е семейство ограничения, които браузърът прилага по тип ресурс, и разликите са това, което я прави експлоатируема.

Четенето е блокирано, зареждането не. Това е най-важната асиметрия в сигурността на браузъра. Твоята страница може свободно да зареди cross-origin изображение, стил, скрипт или iframe. Това, което не може, е да прочете съдържанието.

<!-- Всички тези се зареждат добре cross-origin -->
<img src="https://other.example.com/avatar.png">
<script src="https://cdn.example.com/lib.js"></script>
<iframe src="https://other.example.com/page"></iframe>
// Това е блокирано: четене на тялото на отговора cross-origin
const res = await fetch('https://other.example.com/me');
const body = await res.text(); // TypeError: Failed to fetch

Значи: прати-но-не-чети. Cross-origin заявка се случва, сървърът я обработва, странични ефекти настъпват — браузърът просто отказва да покаже отговора на твоя скрипт. Вземи това присърце и CSRF спира да е изненадващ. CSRF не се нуждае да чете нищо; нужно му е само заявката да пристигне.

Изпълнението на скрипт е специално. Скрипт, зареден чрез <script src> от друг origin, се изпълнява вътре в твоя origin, с пълен достъп до твоя DOM и твоите same-origin права. Той не е в пясъчник. Затова компрометиран CDN е компромет на всеки сайт, който го включва, и затова съществува Subresource Integrity.

DOM-ът е разделен. JavaScript в един фрейм не може да докосне DOM-а на cross-origin фрейм. Разрешеният канал между тях е postMessage:

// Подател
targetFrame.postMessage({ type: 'ping' }, 'https://app.example.com');
 
// Получател — ВИНАГИ проверявай origin-а
window.addEventListener('message', (event) => {
  if (event.origin !== 'https://trusted.example.com') return;
  handle(event.data);
});

Тази проверка на origin постоянно се пропуска, а пропускането ѝ превръща postMessage в отворена врата: всяка страница, която се добере до твоя фрейм, може да му подава данни, на които кодът ти вярва.

CORS: отваряне на вратата нарочно

Same-origin политиката прави легитимните API-та неудобни — фронтенд на app.example.com наистина има нужда да чете api.example.com. CORS е начинът на сървъра да каже „този конкретен origin може да чете отговорите ми“.

Струва си да сме преки за посоката на контрола: CORS не е защита, която сървърът прилага срещу атакуващите. Той е разхлабване, което сървърът дава, наложено от браузъра от името на четящата страница. Неброузърен клиент — curl, скрипт, прокси — игнорира CORS изцяло. CORS пази браузърите на потребителите ти от изтичане на данни към други сайтове. Не пази API-то ти.

Прости заявки

Някои заявки се смятат за достатъчно безобидни, за да се пратят без да се пита първо. Горе-долу: GET, HEAD или POST; без персонализирани заглавки; и Content-Type само application/x-www-form-urlencoded, multipart/form-data или text/plain.

GET /api/me HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Cookie: session=abc

Сървърът отговаря, а браузърът после решава дали да подаде тялото на викащия скрипт:

HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true
Vary: Origin

Забележи какво вече се случи: заявката стигна приложението и всякакви странични ефекти настъпиха, преди браузърът да провери заглавката. Списъкът за разрешение управлява четенето, не изпращането.

Preflight заявки

Всичко извън простия набор — PUT, заглавка Authorization, JSON content type — получава preflight първо:

OPTIONS /api/account HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Access-Control-Request-Method: PATCH
Access-Control-Request-Headers: content-type, authorization
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PATCH
Access-Control-Allow-Headers: content-type, authorization
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 600
Vary: Origin

Само ако това мине, тръгва истинският PATCH. Затова „приемаме само JSON“ е слаба, но истинска спирачка срещу CSRF: обикновена HTML форма не може да произведе Content-Type: application/json, затова подправената заявка на атакуващия би се нуждаела от preflight, който сървърът ти ще откаже.

скрипт на app.example.com api.example.com | | |-- OPTIONS /api/account ------>| Origin + Request-Method |<-- 204 + Allow-* заглавки -----| | | |-- PATCH /api/account -------->| само ако preflight мина |<-- 200 OK ---------------------|

Preflight, после истинската заявка

Грешните конфигурации, които наистина ще намериш

Отразен origin плюс удостоверения. Класическата критична находка:

// Уязвимо: който origin попита, бива одобрен.
res.setHeader('Access-Control-Allow-Origin', req.headers.origin);
res.setHeader('Access-Control-Allow-Credentials', 'true');

Спецификацията забранява Access-Control-Allow-Origin: * заедно с удостоверения точно защото тази комбинация би позволила на всеки сайт да чете удостоверени отговори. Отразяването пресъздава опасността, макар технически да спазва правилото. Сега evil.test може да изпълни fetch('https://api.example.com/me', { credentials: 'include' }) и да чете данните на акаунта ти от собствената ти сесия.

Небрежно съпоставяне на origin. origin.endsWith('example.com') съвпада с notexample.com. origin.startsWith('https://app.example.com') съвпада с https://app.example.com.evil.test. Сравнявай спрямо изричен списък за разрешение с точно равенство на низове.

null в списъка за разрешение. Iframe-и в пясъчник и някои локални файлове пращат Origin: null. Разрешаването на null с удостоверения значи, че всяка страница на атакуващ може да изсече този origin с iframe в пясъчник.

Липсващ Vary: Origin. Ако вариираш заглавката на отговора по origin, но не го казваш, споделен кеш може да сервира allow заглавката на един origin на заявката на друг origin.

Защо е важно

Same-origin политиката е предположението, на което стъпва всеки друг контрол от страната на браузъра. Когато тя държи, злонамерен таб е досада. Когато се счупи, този таб чете имейла ти.

Режимите на провал се нанасят чисто върху цели класове уязвимости. XSS е заобикаляне на same-origin политиката по наследство — кодът на атакуващия влиза вътре в твоя origin, затова политиката услужливо му дава всичко. CSRF експлоатира асиметрията прати-но-не-чети: не трябва четене, само пристигнала заявка. Clickjacking злоупотребява с факта, че фреймването е позволено дори когато четенето не е, затова X-Frame-Options и frame-ancestors съществуват като отделни контроли. Грешна конфигурация на CORS раздава правото на четене доброволно и е единствената от четирите, която един ред код може да създаде и един ред може да поправи.

Практическите правила: дръж изричен списък за разрешение на origin-и с точно съпоставяне, никога не отразявай Origin, никога не комбинирай wildcard или null с удостоверения, винаги праща Vary: Origin и винаги проверявай event.origin в postMessage обработчиците. И помни изречението, което хваща повечето хора неподготвени — CORS ограничава какво браузърите позволяват на страниците да четат; то не дава на API-то ти никаква защита срещу клиент, който просто не го е грижа.

Провери знанията си

2 от 3 за успех

3 въпроса върху това, което току-що прочете. Веднага ще виждаш дали всеки отговор е верен, с кратко обяснение. Дай 2 верни, за да отбележиш урока като завършен.

Не си влязъл, затова този опит няма да бъде запазен. Създай акаунт за да следиш напредъка и да отключваш по-късните модули.