Как чужой сайт заставляет ваш браузер работать против вас
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
Злоумышленник может заманить пользователя по ссылке на свой сайт, где будет перенаправление на этот адрес. Когда вы перейдёте по ссылке, браузер отправит GET-запрос на удаление товара вместе с вашими учётными данными. Сервер выполнит действие, потому что видит ваши учётные данные.
Почему POST-запросы не спасают от CSRF
Многие считают, что POST-запросы безопаснее GET, потому что они не кэшируются и не попадают в историю браузера. Но это не так. POST-запросы так же уязвимы для CSRF, если не использовать дополнительные меры защиты.
Злоумышленник может создать форму с методом POST и отправить её автоматически с помощью JavaScript. Браузер отправит запрос с вашими учётными данными, и сервер выполнит действие.
Как защититься: от простого к сложному
Защита от CSRF строится на нескольких уровнях. Чем больше уровней вы используете, тем надёжнее защита.
Первый уровень: атрибут SameSite для кук
Самый простой способ защититься — использовать атрибут SameSite для кук. Он говорит браузеру, когда отправлять куки вместе с запросом.
SameSite=Strict: браузер отправляет куки только на запросы с вашего сайта. Если пользователь переходит на ваш сайт по ссылке из письма, он не будет залогинен.SameSite=Lax: на запросы с вашего же сайта браузер отправляет куку всегда, а на запросы с чужого — только при переходе на ваш сайт безопасным методом, например по ссылке. На фоновые запросы с чужого сайта (картинки, скрипты, запросы из кода страницы) и на отправку чужой формы методом POST кука не уходит. Это закрывает большинство CSRF-атак, но не все.SameSite=None: браузер отправляет куки на все запросы, даже с чужих сайтов. Это эквивалентно отсутствию защиты.
SameSite=Lax — это компромисс. Он закрывает большинство CSRF-атак, но не ломает пользовательский опыт. Однако он не защищает от всех случаев. Форма с автоотправкой здесь не сработает: на POST-запросы с чужой страницы кука с SameSite=Lax не уходит. Но она уходит на межсайтовый переход по GET — злоумышленнику достаточно заманить пользователя по ссылке, и браузер отправит запрос вместе с кукой.
Второй уровень: токен в форме
Токен в форме — это дополнительная защита. Сервер генерирует уникальный токен для каждой сессии и кладёт его в форму. Когда форма отправляется, сервер проверяет, что токен совпадает с тем, который он выдал.
Токен должен быть привязан к сессии. Иначе злоумышленник откроет форму под своей учётной записью, получит действительный токен и вставит его в поддельную форму, а сервер примет любой токен, который сам когда-то выдал. Если токен лежит только в куке и сервер проверяет его там же, от CSRF это не защищает: браузер отправит куку сам. Кука с токеном работает лишь как вторая копия. Сервер сравнивает её со значением, пришедшим в теле формы или в заголовке, а чужая страница куку прочитать не может и подставить это значение не в состоянии. Простую двойную отправку обходят, если злоумышленник может записать куку на ваш домен, например с поддомена. Поэтому значение в куке надёжнее подписывать на сервере и привязывать к сессии.
Но токен в форме не защищает от XSS. Если злоумышленник может внедрить свой код на ваш сайт, он может прочитать токен и отправить его вместе с запросом.
Третий уровень: проверка заголовков Origin и Referer
Ещё один способ защититься — проверять заголовки Origin или Referer. Они показывают, откуда пришёл запрос. Сервер может проверить, что запрос пришёл с того же домена, что и ваш сайт, и отклонить его, если это не так.
Но у этого способа есть слабые места:
- Подделка невозможна, но заголовки не всегда надёжны: Заголовки
OriginиRefererсо стороны атакующей страницы подделать нельзя — их выставляет сам браузер. Но они могут прийти пустыми или урезанными: их убирают политика отправки источника, прокси и расширения. Тогда проверка либо отклоняет легитимный запрос, либо пропускает подозрительный, поэтому сама по себе она решения не даёт. - Если ваш сайт доступен по HTTP и HTTPS, заголовки могут быть разными.
- Если ваш сайт использует iframe, заголовки могут быть пустыми.
Поэтому полагаться только на них нельзя. Это дополнительная мера, а не основная.
Почему защита от CSRF — это не только про настройки браузера
Многие считают, что если закрыться настройкой CORS или SameSite, то CSRF не страшен. Но это не так.
CORS не запрещает межсайтовые запросы, а решает, может ли скрипт чужой страницы прочитать ответ. Простые запросы — отправку формы, POST с обычным типом тела и без нестандартных заголовков — браузер отправляет без предварительной проверки, и учётные данные уходят вместе с ними, если их не удерживает атрибут SameSite. Сервер выполняет действие раньше, чем браузер решит, отдавать ли ответ скрипту, поэтому от CSRF CORS не защищает.
SameSite закрывает большую часть случаев, но не все. Если ваш сайт принимает GET-запросы для изменения состояния, атака всё ещё возможна. Кроме того, SameSite не работает в старых браузерах.
Защита от CSRF должна быть многоуровневой. Используйте SameSite=Lax для кук, токен в форме и проверку заголовков Origin и Referer. Это закрывает большинство случаев, но не все.
Тупик: почему нельзя полагаться только на настройки браузера
Настройки браузера — это важная часть защиты, но они не решают проблему полностью. Вот почему:
- GET-запросы: Если ваш сайт принимает GET-запросы для изменения состояния,
SameSite=Laxне защитит от CSRF. Злоумышленнику достаточно заманить пользователя по ссылке, и браузер отправит запрос вместе с учётными данными. - Старые браузеры:
SameSiteне поддерживается в старых браузерах. Если ваши пользователи используют старые версии браузеров, они остаются уязвимыми. - Пустые заголовки: Заголовки
OriginиRefererсо стороны атакующей страницы подделать нельзя, но они могут прийти пустыми или урезанными. Некоторые браузеры и расширения блокируют их по соображениям приватности, а прокси могут их модифицировать. - XSS: Если злоумышленник может внедрить свой код на ваш сайт, он может прочитать токен в форме и отправить его вместе с запросом. Поэтому защита от CSRF должна идти рука об руку с защитой от XSS.
Позиция редакции
CSRF — это атака на доверие. Сервер доверяет браузеру, а браузер доверяет учётным данным. Чтобы защититься, нужно сломать эту цепочку доверия.
Самый надёжный способ — использовать многоуровневую защиту:
- SameSite=Lax для кук: На запросы с вашего сайта браузер отправляет куки всегда, а на запросы с чужого — только при переходе верхнего уровня безопасным методом, например по ссылке. Это закрывает большинство CSRF-атак, но не ломает пользовательский опыт.
- Токен в форме: Это дополнительная защита, которая работает даже в старых браузерах.
- Проверка заголовков Origin и Referer: Это дополнительная мера, которая помогает отсеять подозрительные запросы, если заголовки присутствуют и не модифицированы.
Но даже эти меры не защищают от всех случаев. Если ваш сайт принимает GET-запросы для изменения состояния, их нужно переделать в POST.
Условие, при котором наша позиция неверна: браузер не прикладывает учётные данные к запросам сам. Если вход подтверждается токеном в заголовке Authorization, который код страницы выставляет явно, чужая страница не может заставить браузер его приложить, и все три уровня защиты этим действиям не нужны. Но если вместо кук работает базовая HTTP-аутентификация, клиентский сертификат или доверие по адресу внутренней сети, запрос с чужой страницы снова придёт с этими учётными данными, и защита нужна. Только без первого уровня: SameSite — атрибут куки и на эти способы входа не действует, так что держат оборону токен в форме и проверка Origin.