Браузерная безопасность (часть 1)
Сегодня хочу начать небольшую серию постов про браузерную безопасность и CSRF. В браузере есть несколько механизмов, которые решают: — кто может отправлять запросы; — кто может читать ответы; — когда браузер сам приложит авторизацию; — какой формат тела запроса вообще можно подделать простой HTML-формой.
━━━━━━━━━━ CORS и SOP ━━━━━━━━━━
Начну с базы — SOP и CORS. 🔐 SOP, или Same-Origin Policy, — это политика безопасности браузера. Её задача — не дать JavaScript-коду с одного origin читать ответы другого origin.
Origin состоит из трёх частей: scheme + host + port
То есть для браузера важны: • протокол: HTTP или HTTPS; • домен или поддомен; • порт.
Если отличается хотя бы одна часть — это уже другой origin. Например, frontend может жить на одном origin, API на другом, CDN на третьем. Для современной архитектуры это нормально, но для строгой SOP-модели такие сервисы уже “чужие” друг для друга.
И тут появляется CORS. CORS — это механизм, который позволяет серверу явно сказать браузеру: 💬 “Вот этому origin я разрешаю читать мой ответ”.
У CORS есть простые и сложные запросы.
Простой запрос — это такой cross-origin запрос, который браузер отправляет сразу, без предварительной проверки OPTIONS. Это: • GET • HEAD • POST Но не любой POST, а только с ограниченными заголовками и ограниченным Content-Type: • application/x-www-form-urlencoded • multipart/form-data • text/plain
Почему simple requests вообще существуют? Потому что до появления CORS браузеры уже умели делать такие запросы через обычные HTML-механизмы. Тем самым решается то как не сломать старую веб-совместимость. С точки зрения безопасности, простые запросы не имеют каких-либо уязвимостей по сравнению со сложными.
Сложные / preflighted requests - это запрос, перед которым браузер сначала спрашивает сервер: "Можно ли мне отправить такой cross-origin запрос?" через заголовок OPTIONS
Более подробно про то, как работает CORS, можно почитать в моём отдельном посте: ➥ https://setka.ru/posts/019e9db2-473c-7e10-87bb-6ecee5994ed4
Но здесь важен один нюанс, который часто ломает понимание CSRF: CORS в основном контролирует не сам факт отправки запроса, а возможность JavaScript-кода прочитать ответ.
То есть запрос может физически уйти на сервер. Сервер может даже выполнить действие. Но если CORS не разрешил текущий Origin, браузер не отдаст ответ JavaScript-коду.
━━━━━━━━━━ Влияние контента на безопасность ━━━━━━━━━━
Теперь переходим к другой важной части — Content-Type.
Content-Type описывает, в каком формате передаётся тело запроса. Например: • application/json — JSON-структура; • application/xml — XML; • text/plain — обычный текст; • application/x-www-form-urlencoded — классическая HTML-форма; • multipart/form-data — форма с файлами или сложными полями.
И вот здесь начинается интересное для эксплуатации атаки CSRF. Классическая CSRF-атака часто строится на том, что атакующий создаёт HTML-форму и заставляет браузер жертвы отправить её на целевой сайт. Проблема для атакующего в том, что обычная HTML-форма не умеет отправлять application/json. Она умеет отправлять только ограниченные форматы, которые браузер считает “простыми”: form-urlencoded, multipart/form-data и text/plain.
Поэтому если API строго принимает только JSON, проверяет Content-Type и не парсит text/plain или form-urlencoded как JSON, то классическая CSRF-атака становится невозможной.
Потому что если атакующий попробует отправить JSON через JS-запрос, браузер, скорее всего, сделает preflight. То есть сначала отправит OPTIONS-запрос и спросит сервер, то атакующий origin не разрешён и запрос не будет отправлен.
❗️Но если backend говорит “only JSON API”, но на деле принимает text/plain как JSON, игнорирует Content-Type, то защита не отработает и тем самым CORS не является панацеей для CSRF атак.
Вот и первая часть подходит к концу и во второй части уже я разберу SameSite, HttpOnly, разницу между site и origin, а также как хранение токенов спасает от классической CSRF атаки.