Same-Origin политика и CORS
Единствената истинска граница на сигурността в браузъра и заглавката, която нарочно пробива дупки в нея.
Same-origin политиката е единствената смислена граница на сигурността в браузъра. Всичко друго — флагове, заглавки, токени — е усъвършенстване около нея. CORS е механизмът за нейното нарочно разхлабване, а зле конфигуриран CORS е една от най-честите находки при всяка истинска оценка на API.
Какво е origin
Origin е кортеж от три неща:
схема :// хост : портДва URL адреса са same-origin само ако и трите съвпадат точно. Без частични точки.
| URL A | URL B | Same origin? |
|---|---|---|
https://app.example.com/a | https://app.example.com/b | Да — пътят е без значение |
https://app.example.com | http://app.example.com | Не — схемата се различава |
https://app.example.com | https://api.example.com | Не — хостът се различава |
https://app.example.com | https://app.example.com:8443 | Не — портът се различава |
https://example.com | https://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, authorizationHTTP/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 ---------------------|
Грешните конфигурации, които наистина ще намериш
Отразен 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-то ти никаква защита срещу клиент, който просто не го е грижа.
Провери знанията си
3 въпроса върху това, което току-що прочете. Веднага ще виждаш дали всеки отговор е верен, с кратко обяснение. Дай 2 верни, за да отбележиш урока като завършен.
Не си влязъл, затова този опит няма да бъде запазен. Създай акаунт за да следиш напредъка и да отключваш по-късните модули.