Мы мучились с CSRF, прямо скажем, целую вечность и наконец поняли, как с этой проблемой бороться. Решение – куки с параметром SameSite. Такую защиту легко создать и еще легче ей пользоваться. Один раз установили и забыли: дальше куки будут работать без сбоев.

Проблема стара как сам интернет

CSRF-атаки существовали всегда. Атаки такого типа в принципе возможны, потому что сайты могут отправлять запросы другим сайтам. Представим, что я встроил в эту страницу вот такую форму:

<form id="stealMoney" action="https://your-bank.com/transfer" method="POST">
    <input name="amount" type="hidden" value="&pound;1,000"></input>
</form>

Ваш браузер загружает страницу и форму вместе с ней, а я потом перенаправляю эту форму с помощью простого фрагмента JS-кода:

document.getElementById("stealMoney").submit();

CSRF – это английская аббревиатура фразы Cross Site Request Forgery, которая переводится, как межсайтовая подделка запроса. Этот вид атак называется так неслучайно. Я подделываю запрос, который потом отправляется с моего сайта на сайт вашего банка (то есть между сайтами).

И проблема даже не в том, что я отправляю запрос, а в том, что ваш браузер вместе с ним высылает все ваши куки. Запрос приходит с учетом тех прав доступа, которые у вас есть на сайте банка в данный момент. То есть если вы были залогинены, то всё, считайте, вы мне перевели 1000 фунтов. Спасибочки! Если вы не были авторизованы, запрос вреда не причинит (потому что без авторизации в личном кабинете никакие переводы средств сделать невозможно).

Как снизить риск CSRF-атак

Банк может защититься от CSRF-атак несколькими способами. Я не буду их подробно разбирать, потому что на эту тему есть масса информации в интернете. Ограничусь кратким обзором, чтобы показать, как они работают. Итак, есть два пути:

1. Проверять источник

Теоретически запрос содержит два фрагмента данных, которые указывают, откуда этот запрос пришел. Это заголовки Origin и Referer. Проверьте один или оба этих параметра, чтобы убедиться, что это ваш запрос. Если запрос идет с другого ресурса, выкидываем его без лишних раздумий. Браузеры, конечно, защищают эти заголовки от хакеров, но иногда они могут отсутствовать:

accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8  
accept-encoding: gzip, deflate, br  
cache-control: max-age=0  
content-length: 166  
content-type: application/x-www-form-urlencoded  
dnt: 1  
origin: https://report-uri.io  
referer: https://report-uri.io/login  
upgrade-insecure-requests: 1  
user-agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36

2. Использовать защитные токены

Есть два сценария защиты сайта от CSRF-атак с помощью токенов. Они хоть и разные, но принцип у них один. Допустим, пользователь запрашивает страницу для перевода денег, как в примере выше. Ваш сервер прикрепляет произвольный токен к форме на странице в ответ на запрос. Если пользователь не мошенник, от него обратно придет форма с тем же токеном, который выслал сервер. Если же происходит CSRF-атака, злоумышленник никогда не сможет получить правильное значение токена, потому что прочитать ответ на запрос может только скрипт с того же домена (так работает политика единого домена). Этот способ хорошо защищает, но сервер должен обязательно отслеживать отправляемые и получаемые защитные токены. Или можно сделать так: в форму вставляется токен, и браузер пользователя создаёт куку с таким же значением. Когда реальный пользователь отправляет форму, значения токена в форме и в куках совпадают. Когда же форма приходит от мошенника, в куках его браузера нет значения токена, и атакующий не проходит проверку.

<form action="https://report-uri.io/login/auth" method="POST">
  <button class="btn btn-primary" type="submit">Login</button>
</form>

Что не так с этими способами

Оба способа долгое время надежно отражали CSRF-атаки. Проверка заголовков Origin и Referer не дает 100% защиты, поэтому большинство сайтов обычно прибегали к одному из сценариев с защитными токенами. Но у всех этих техник есть один минус: предполагается, что именно сайт должен искать решение проблемы. Да, описанные способы не такие уж сложные технически, но, тем не менее: нам постоянно приходится искать выход из ситуации, когда браузер делает какую-то штуку, которая нам не нравится. Почему бы нам просто не запретить браузеру делать то, что нам не нужно?

Куки с параметром SameSite

Принцип такой: куки с параметром SameSite безжалостно уничтожают CSRF-атаки. Напрочь. Все, нет больше атак. Финита. Пока! Такие куки – именно то, что нужно, чтобы выиграть битву за безопасность в интернете, потому что параметр SameSite ну очень легко установить. Для примера возьмем ваши текущие куки:

Set-Cookie: sess=abc123; path=/

Теперь просто добавим параметр SameSite.

Set-Cookie: sess=abc123; path=/; SameSite

Дело сделано! Я серьезно, больше ничего не нужно! Когда вы добавите этот параметр к куки-файлу, браузер сможет обеспечить этому файлу определенную защиту. Вы можете выбрать между строгим и нестрогим режимами в зависимости от того, насколько вы серьезно настроены. Когда вы только подключаете параметр SameSite, он по умолчанию работает в строгом режиме, но вы также можете конкретно прописать, какой именно режим вам нужен.

SameSite=Strict
SameSite=Lax

Строгий режим (Strict)

Строгий режим – это очевидно наиболее предпочтительная версия, но нестрогий вариант существует, потому что все сайты разные, и у всех разные требования. Если вы работаете в строгом режиме, браузер не отправляет куки в ответ на межсайтовые запросы. Вообще никогда. То есть у CSRF-атак нет шансов. Единственная проблема, с которой вы, вероятно столкнетесь – при навигации по сайту (когда меняется url в адресной строке) браузер отправлять куки тоже не будет. Если бы я скинул вам ссылку на https://facebook.com, и Facebook бы использовал строгий режим (Strict mode), то когда бы вы кликнули на ссылку, оказалось бы, что вы не залогинены. Причем неважно, были ли вы залогинены до этого или нет. Открыв ссылку в новой вкладке, вы в любом случае не были бы залогинены в Facebook. Этот может немного раздражать или удивлять пользователей (или и то, и другое), но зато обеспечивает невероятно прочную защиту. Чтобы использовать строгую защиту, Facebook нужно было бы брать пример с сайта Amazon, у которого два типа куки-файлов. Один тип – это базовые куки: вас идентифицируют как пользователя и позволяют оставаться залогиненным. Но если вы захотите совершить действия, несущие риск уязвимости, например, сделать покупку или изменить что-то в своем аккаунте, вам потребуются вторые куки, «настоящие» куки, которые позволяют делать важные вещи. Первым кукам в этом случае не нужно обладать набором параметров SameSite, потому что это «удобные» куки, которые на самом деле не дают вам делать рискованные вещи. И если злоумышленник сделает межсайтовый запрос по этим кукам, ничего не произойдет. А вот чувствительные куки будут наделены параметром SameSite, и злоумышленник не сможет взломать сайт с помощью межсайтовых запросов. Это идеальное решение и для пользователя, и для безопасности. Но этот вариант не всегда возможен. И к тому же хотелось бы, чтобы куки с параметром SameSite было легко применять. Для этого существует еще одна опция.

Нестрогий режим (Lax)

Установка нестрого режима для SameSite защиты решает проблему, указанную выше, когда при строгом режиме пользователь идет по ссылке на целевой сайт и автоматически разлогинивается там. В нестрогом режиме есть единственное исключение, которое позволяет куки-файлам прикрепляться к навигации верхнего уровня, в которой используется безопасный HTTP метод. «Безопасные» HTTP методы определены в разделе 4.2.1 RFC 7321 как запросы типов GET, HEAD, OPTIONS и TRACE. Нас здесь интересует запрос GET. Работает он так: при переходе по ссылке на https://facebook.com запрос браузера использует куки с параметром SameSite. Пользователь не разлогинивается и продолжает работу с комфортом. И при этом мы полностью защищены от CSRF атак по запросу POST. Вспомним пример с переводом денег в самом начале статьи. При использовании параметра SameSite такая атака провалится даже в нестрогом режиме.

<form id="stealMoney" action="https://your-bank.com/transfer" method="POST">
    <input name="amount" type="hidden" value="&pound;1,000"></input>
</form>

Поскольку запросы POST не считаются безопасными, браузер не будет прикреплять к ним куки. Правда, злоумышленник может спокойно поменять запрос на безопасный и повторить атаку.

<form id="stealMoney" action="https://your-bank.com/transfer" method="GET">
    <input name="amount" type="hidden" value="&pound;1,000"></input>
</form>

Пока мы не принимаем запросы GET вместо запросов POST, эти атаки бесполезны. Но за этим моментом нужно следить, работая в нестрогом режиме. Также, если злоумышленник сможет запустить навигацию верхнего уровня или открытие нового окна, ему удастся заставить браузер выдать запрос GET с прикрепленными куками. Но таков компромисс при работе в нестрогом режиме. Мы сохраняем удобство для пользователя, расплачиваясь за это небольшими рисками.

Дополнительные возможности

В этой статье рассказывается, как с помощью куков с параметром SameSite обезопасить сайт от от CSRF-атак. Но, как вы уже могли догадаться, у этого механизма существуют и другие варианты применения. Во-первых, в спецификациях обычно упоминается атака Cross-Site Script Inclusion (XSSI), при которой вредоносный код отправляет запрос сайту с пейлоадом и в ответ может получить разный пейлоад в зависимости от того, залогинен юзер или нет. В случае межсайтового запроса злоумышленник не сможет эксплуатировать уязвимость из-за использованием кук с параметром SameSite для получения другого ответа. Также здесь разобраны несколько “атак по времени”, от которых может помочь эта защита.

Другой интересный вариант применения, который нигде не разбирается подробно – это защита против утечки содержания куки-сессий во время атак, исполненная по примеру BEAST, с использованием алгоритмов сжатия данных (CRIME, BREACH, HEIST, TIME). Это по-настоящему серьезный уровень, но базовый сценарий – это «атака посредника» (MiTM – Man in the middle). С ее помощью злоумышленники заставляют браузер выполнять межсайтовые запросы через любой механизм и отслеживать их. Меняя размер данных в запросах от браузера и наблюдая за ответом сервера, атакующий может вычислить значение идентификатора сессии по одному байту за запрос, но когда подключен параметр SameSite, браузер не добавляет куки в такие запросы, и мошенник не может вычислить их значение.

Поддержка в браузерах

Можете быть уверены, что Firefox и Chrome первыми предлагают большинство новых инструментов безопасности. Куки с параметром SameSite не исключение. Chrome поддерживает этот механизм со времен выхода версии 51, а значит Opera, браузер Android и Сhrome на Android также его поддерживают. На caniuse.com есть постоянно обновляемый список браузеров, поддерживающих куки с параметром SameSite. Можете свериться с ним. У Firefox пока нет поддержки параметра SameSite, но есть открытый тикет, где проследить за разработкой.

Поддержка параметра SameSite еще не распространена широко, но мы всем советуем добавлять его в куки. Браузеры, которые считывают этот параметр, примут настройки и дадут дополнительную защиту, в то время как остальные просто будут игнорировать его и работать по-старому. Вы ничего не потеряете, зато сформируете очень тщательный подход к защите. Пройдет еще много времени, прежде чем мы откажемся от традиционных механизмов борьбы с CSRF. Пока что мы можем существенно укрепить оборону, используя вместе с ними параметр SameSite.

Перевод оригинальной статьи: «Cross-Site Request Forgery is dead!» by Scott Helme