Как чужой сайт заставляет ваш браузер работать против вас
CSRF — это не ошибка в коде, а особенность работы браузера. Когда вы заходите на сайт, браузер автоматически прикрепляет к каждому запросу на этот домен все куки, которые для него установлены. Неважно, откуда пришёл запрос — с вашей страницы или с чужого сайта. Браузер не спрашивает разрешения, он просто делает это.
Представьте, что вы залогинены в интернет-магазине. Открываете письмо с рекламой, где картинка на самом деле — ссылка на удаление всех товаров из корзины. Браузер отправляет запрос на удаление, прикрепляет куки сессии, и сервер выполняет действие. Вы ничего не замечаете, но ваша корзина пуста.
Почему браузер не видит разницы между вашим запросом и чужим
Механизм сессий построен на доверии. Сервер выдаёт вам куку с идентификатором сессии, а браузер честно отправляет её обратно при каждом запросе. Сервер не проверяет, откуда пришёл запрос — он видит только куку и считает, что это вы.
Злоумышленник не может прочитать ваши куки, но он может заставить ваш браузер отправить их на сервер. Для этого достаточно вставить на свой сайт картинку, скрипт или форму, которая отправляет запрос на ваш сервер. Браузер сделает это автоматически, потому что так работает HTTP.
Чем CSRF отличается от кражи данных
CSRF — это атака на действие, а не на данные. Злоумышленник не получает доступ к вашей информации, но может заставить сервер выполнить действие от вашего имени: перевести деньги, сменить почту, удалить аккаунт.
Отличие от XSS здесь принципиальное. XSS — это когда злоумышленник внедряет свой код в ваш сайт и получает доступ к данным или выполняет действия от вашего имени. CSRF не требует внедрения кода: браузер сам отправляет запрос с вашими куками.
Защита от XSS не закрывает CSRF, потому что механизмы разные. XSS эксплуатирует уязвимости в коде вашего сайта, а CSRF эксплуатирует доверие сервера к браузеру.
Как работает атака: от теории к практике
Чтобы понять, как защититься, нужно понять, как работает атака. Представим, что у вас есть сайт с формой для смены пароля:
<form action="/change-password" method="POST">
<input type="password" name="new_password">
<button type="submit">Сменить пароль</button>
</form>
Злоумышленник создаёт свой сайт с такой формой:
<form action="https://your-site.com/change-password" method="POST">
<input type="hidden" name="new_password" value="hacked">
</form>
<script>document.forms[0].submit();</script>
Когда вы заходите на сайт злоумышленника, браузер автоматически отправляет форму на ваш сайт с вашими куками. Сервер видит запрос с вашей сессией и меняет пароль.
Почему GET-запросы опаснее, чем кажутся
Многие разработчики считают, что GET-запросы безопасны, потому что они не меняют состояние. Но это не так. Если GET-запрос меняет состояние, он открывает дверь для CSRF.
Представьте, что ваш сайт принимает GET-запрос для удаления товара из корзины:
GET /cart/delete?id=123
Злоумышленник может вставить на свой сайт картинку с таким адресом:
<img src="https://your-site.com/cart/delete?id=123" width="1" height="1">
Когда вы заходите на сайт злоумышленника, браузер загружает картинку и отправляет GET-запрос на удаление товара. Сервер выполняет действие, потому что видит ваши куки.
Почему POST-запросы не спасают от CSRF
Многие считают, что POST-запросы безопаснее GET, потому что они не кэшируются и не попадают в историю браузера. Но это не так. POST-запросы так же уязвимы для CSRF, если не использовать дополнительные меры защиты.
Злоумышленник может создать форму с методом POST и отправить её автоматически с помощью JavaScript. Браузер отправит запрос с вашими куками, и сервер выполнит действие.
Как защититься: от простого к сложному
Защита от CSRF строится на нескольких уровнях. Чем больше уровней вы используете, тем надёжнее защита.
Первый уровень: атрибут SameSite для кук
Самый простой способ защититься — использовать атрибут SameSite для кук. Он говорит браузеру, когда отправлять куки вместе с запросом.
SameSite=Strict: браузер отправляет куки только на запросы с вашего сайта. Если пользователь переходит на ваш сайт по ссылке из письма, он не будет залогинен.SameSite=Lax: браузер отправляет куки только на навигационные запросы (например, при переходе по ссылке), но не на фоновые запросы (например, при загрузке картинки). Это закрывает большинство CSRF-атак, но не все.SameSite=None: браузер отправляет куки на все запросы, даже с чужих сайтов. Это эквивалентно отсутствию защиты.
SameSite=Lax — это компромисс. Он закрывает большинство CSRF-атак, но не ломает пользовательский опыт. Однако он не защищает от всех случаев. Например, если ваш сайт принимает GET-запросы для изменения состояния, атака всё ещё возможна.
Второй уровень: токен в форме
Токен в форме — это дополнительная защита. Сервер генерирует уникальный токен для каждой сессии и кладёт его в форму. Когда форма отправляется, сервер проверяет, что токен совпадает с тем, который он выдал.
Токен должен быть привязан к сессии, потому что иначе злоумышленник может сгенерировать его сам. Если токен лежит в куках, это не защищает от CSRF: браузер всё равно отправит куки вместе с запросом.
Но токен в форме не защищает от XSS. Если злоумышленник может внедрить свой код на ваш сайт, он может прочитать токен и отправить его вместе с запросом.
Третий уровень: проверка заголовков Origin и Referer
Ещё один способ защититься — проверять заголовки Origin или Referer. Они показывают, откуда пришёл запрос. Если запрос пришёл с чужого сайта, сервер может его отклонить.
Но у этого способа есть слабые места:
- Заголовки
OriginиRefererможно подделать или убрать. Некоторые браузеры и расширения блокируют их по соображениям приватности. - Если ваш сайт доступен по HTTP и HTTPS, заголовки могут быть разными.
- Если ваш сайт использует iframe, заголовки могут быть пустыми.
Поэтому полагаться только на них нельзя. Это дополнительная мера, а не основная.
Почему защита от CSRF — это не только про настройки браузера
Многие считают, что если закрыться настройкой CORS или SameSite, то CSRF не страшен. Но это не так.
CORS защищает от межсайтовых запросов, но не от CSRF. Если злоумышленник может заставить браузер отправить запрос на ваш сервер, CORS не поможет: браузер всё равно отправит куки.
SameSite закрывает большую часть случаев, но не все. Например, если ваш сайт принимает GET-запросы для изменения состояния, атака всё ещё возможна. Кроме того, SameSite не работает в старых браузерах.
Защита от CSRF должна быть многоуровневой. Используйте SameSite=Lax для кук, токен в форме и проверку заголовков Origin и Referer. Это закрывает большинство случаев, но не все.
Тупик: почему нельзя полагаться только на настройки браузера
Настройки браузера — это важная часть защиты, но они не решают проблему полностью. Вот почему:
- GET-запросы: Если ваш сайт принимает GET-запросы для изменения состояния,
SameSiteне защитит от CSRF. Злоумышленник может вставить картинку или скрипт с таким запросом, и браузер отправит его с вашими куками. - Старые браузеры:
SameSiteне поддерживается в старых браузерах. Если ваши пользователи используют старые версии браузеров, они остаются уязвимыми. - Подделка заголовков: Заголовки
OriginиRefererможно подделать или убрать. Некоторые браузеры и расширения блокируют их по соображениям приватности. - XSS: Если злоумышленник может внедрить свой код на ваш сайт, он может прочитать токен в форме и отправить его вместе с запросом. Поэтому защита от CSRF должна идти рука об руку с защитой от XSS.
Позиция редакции
CSRF — это атака на доверие. Сервер доверяет браузеру, а браузер доверяет кукам. Чтобы защититься, нужно сломать эту цепочку доверия.
Самый надёжный способ — использовать многоуровневую защиту:
- SameSite=Lax для кук: Это закрывает большинство CSRF-атак, но не ломает пользовательский опыт.
- Токен в форме: Это дополнительная защита, которая работает даже в старых браузерах.
- Проверка заголовков Origin и Referer: Это дополнительная мера, которая помогает отсеять подозрительные запросы.
Но даже эти меры не защищают от всех случаев. Если ваш сайт принимает GET-запросы для изменения состояния, их нужно переделать в POST. Если злоумышленник может внедрить свой код на ваш сайт, он может прочитать токен и отправить его вместе с запросом.
Условие, при котором наша позиция неверна: если ваш сайт не хранит состояние и не использует куки для аутентификации. В этом случае CSRF не страшен, потому что нет сессии, которую можно эксплуатировать. Но таких сайтов мало, и большинство из них всё равно используют куки для других целей.