csrf: токены не нужны?

от автора

بسم الله الرحمن الرحيم‎‎

Для защиты от CSRF вы должны использовать анти-CSRF токены и только их. © pyrk2142
Типичные ошибки при защите сайтов от CSRF-атак

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

И еще. Коллеги, если при прочтении статьи у вас на каком либо моменте возникает яростное несогласие со мной — не торопитесь писать разоблачительные комментарии — дочитайте пожалуйста статью до конца. Я излагал мысли по мере их возникновения и в ходе осмысления тоже менял отношение к ним.

Что мы подразумеваем под csrf атакой?

Вольный пересказ вики:
CSRF (англ. Сross Site Request Forgery — «Межсайтовая подделка запроса», также известен как XSRF) — вид атак на посетителей веб-сайтов. Жертва, авторизованная на сайте А(target), заходит на сайт В(зловред), в ходе просмотра которого код, внедренный в страницу сайта В отправляет запрос к сайту А. Поскольку пользователь авторизован на сайте А, при отправке запроса передаются куки, таким образом сайт А предполагает что запрос порядочный и выполняет его. Сайт А в этой ситуации признается уязвимым к csrf атаке.

Вводная:

  • javascript в броузере пользователя включен
  • броузер отправляет/принимает (и обрабатывает) http headers согласно спецификациям (не пригодилось)
  • уязвимости броузеров не принимаем во внимание
  • отклонения от стандартов в поведении броузеров принимаем во внимание
  • сайт А — работает только по https, перехват трафика не рассматривается

Как предлагается защищаться от атаки в интернетах?
Генерируем токен (мандат), прикручиваем его к форме (скрытым полем) и при отправке запроса на сервер передаем в том числе и его.
На сервере проверяем валидность токена (и возможно на какой вид операции выдан) и если все в порядке — выполняем запрос, иначе с негодованием отвергаем.
Как это работает? Когда зловред формирует запрос к сайту A(через страницу сайта В, открытую в броузере авторизованного на сайте А клиента), броузер автоматически передает при запросе куку (пользователь то авторизован). Соответственно помимо куки должен быть секрет, не передающийся автоматически серверу (то есть не хранящийся в куках) и недоступный зловреду. Это и есть токен.

То есть токен служит гарантией того, что запрос сформирован контролируемым нами кодом. Есть ли другие способы понять, какой код сформировал запрос? Да, есть один, и он до безобразия прост.
HTTP Referer
Проверяя реферер, мы гарантированно отсеиваем запросы, сформированные не с наших страниц. Так ли это?
В интернетах полно утверждений что этот способ ненадежен, так как реферер легко подделать. Это, мягко говоря, не так. Давайте не будем выпадать из контекста — зловреду надо подделать запрос из броузера, в котором сидит авторизованный на нашем сайте клиент. Так вот, этот реферер зловред подделать не может. Либо уйдет реферером url зловредного сайта либо пустой реферер. Если мы на своей стороне (на сервере) делаем проверку сессии (куки) и реферера (на принадлежность нашему домену) отсеивая пустые и левые рефереры — то злоумышленик не сможет просочиться.
Именно обработка пустых рефереров влияет на надежность схемы: отбрасываем — схема надежная, принимаем — подставляем клиента. Именно многочисленные реализации второго сценария сформировали мнение о слабости схемы.
А как же быть с подделкой реферера? Она есть и это факт. Но зловред может подделать реферер только со своего компа (подконтрольного), на котором не будет сессии (куки). Таким образом злоумышленник не может собрать в одном месте поддельный реферер и куку сессии для осуществления поддельного запроса. Схема надежная, токены не нужны. Это одна из двух схем, которые я пока что выбрал для себя. Итого:

Решение #1
Смотрим реферер, если пустой или не наш домен — шлем в лес. Для проектов без cross domain requests, небольшая часть пользователей (пара процентов) испытает неудобства. Можно разрешить CORS и вести wite list доверенных доменов.

Но эта схема не подойдет для проектов, принимающих запросы с неограниченного числа сайтов (cross domain ajax, например кнопка like у facebook и тому подобное).

Тут из зала раздается вопрос — как быть с пользователями, чьи рефереры не доходят до сервера? Да, есть такое утверждение. Оно кочует в интернетах, на моей памяти, лет уже как десять. Ходят слухи, что есть прокси, которые режут хидеры и реферер не проходит. Остается загадкой, как проходит тогда кука, ну да ладно. На мой взгляд, тот процент посетителей без хидеров формируется из:

  • зашли по ссылке из закладок (на статейниках такое сплошь и рядом)
  • серфят в анонимном режиме (самая большая, на мой вгляд группа)
  • открыли ссылку в письме с почтового клиента (мало таких, но есть. Особенно если вы продвигаете сайт спамом)
  • боты. Не только яндекс и гугл, в сети есть сотни проектов, индексирующих интернеты. (да, не стоит недооценивать сеошников)
  • прокся порезала хидер (наверно есть и такие)

По хорошему нужен обзор проксей (ПО) с дефолтными настройками на предмет резки заголовков, а также настройки, которые к этому приводят с пониманием, зачем именно такие настройки нужны. Такого обзора не нашел, кто составит — буду благодарен. А пока примем за данность что такие прокси есть и пользователи, которые сидят за ними — для нас важны.

Ок. То есть реферер роли не играет (мы хотим обслуживать клиентов без реферера). В этом случае токен нужен. Но! Есть нюансы.
Первое, второе и десерт — на каком этапе появляется токен, как долго живет и где хранится?
Множество ресурсов рекомендует создавать токен при авторизации и использовать его на время сессии. То есть токен, получается, брат-близнец куки, просто хранится в другом месте. Нормальное такое решение. Все меняется когда приходят они. Нюансы.
Как долго живет сессия? Если до момента закрытия страницы, то все нормально. Закрыли страницу, сессия померла, при повторном открытии требуется авторизация, после прохождения которой будет выдан токен.
Что если мы хотим запомнить пользователя надолго? При закрытии страницы кука остается, при повторном открытии страницы пользователь авторизован, все довольны. Вопрос — откуда взять токен? Этот же вопрос возникает при переходе на другую страницу авторизованным пользователем. Отдать токен в теле страницы при открытии? Тогда мы откатываемся к началу, так как — если мы отдаем верстку страницы с токеном при запросе авторизованного пользователя, то мы отдадим токен зловреду, который запросит ее через авторизованного пользователя, отпарсит страницу, вытащит токен и сформирует злозапрос. То есть в этой ситуации вместо одного запроса для совершения злодеяния потребуется два. Не более того. Но! Это праведливо если мы разрешили cross domain requests через CORS. Если CORS не нужен, то все в порядке. То есть:

Решение #2
На реферер не смотрим, генерим токен при авторизации, прибиваем гвоздями к верстке, время жизни куки — до закрытия страницы. Cross domain requests, CORS, (см. ниже) — влияния на безопасность не оказывает. Годится для одностраничных web-приложений. При перезагрузке страницы необходима повторная авторизация (либо см. решение #3)

Решение #3
На реферер не смотрим, генерим токен при авторизации, прибиваем гвоздями к верстке, время жизни куки — любое. Cross domain requests, CORS, (см. ниже) — запрещены. При загрузке страницы авторизованным клиентом токен повторно прибивается к верстке. Годится для многостраничников.

Тут хочу отметить деталь в работе CORS. Атака зловреда сорвется именно из-за токена а не из-за запрещенного CORS. Первый запрос на чтение страницы с мандатом пройдет до сервера и ВЫПОЛНИТСЯ им (если мы не обрабатываем хидер origin до обработки запроса), ответ вернется клиенту, но не отдастся злоскрипту, таким образом тот не сможет выделить токен. Если мы работаем без токена, то запрет CORS нас никак не спасет — зловред может сформировать запрос, который дойдет до сервера и выполнится им, просто зловреду не видно будет ответа сервера, что не всегда и нужно. Так что хинт: не пренебрегайте вниманием к хидеру origin. В нашем случае это синоним реферера. Казалось бы — а зачем? Да, на безопасность в этом сценарии не влияет, но проверка origin (как и реферер) до обработки позволит отклонять левые запросы без их обработки. Может влиять на производительность ( например от имени авторизованного пользователя зловред может запустить ядреный поиск, недоступный не авторизованным пользователям. Результат не увидит, а вот серваку вашему возможно придется покряхтеть винтами)

Но вот нам в голову приходит светлая мысль улучшить не только наш сайт но и весь интернет и мы начинаем распространять новый модный сервис с кнопочками, лайками и фоловами.
То есть надо разрешить CORS. О, в этот момент все преображается и начинает играть новыми красками!
Как только мы разрешаем CORS, мы теряем возможность хранить токен в верстке страницы (см. выше). Но хранить его где-то надо. Надеюсь, отчаянность мысли хранить токен в куках объяснять не надо.
Путем нехитрых умозаключений приходим к простому пониманию — транспортировка данных от сервера до клиента у нас идет по двум маршрутам — http headers и верстка страницы (либо json ответ, в данном контексте не важно). Все, других путей нет. Хидеры отпадают (да, XMLHTTPRequest дает доступ к хидерам от сервера, но все что можем прочитать мы — прочитает зловред), остается верстка. Но в верстку тоже нельзя, мы уже разобрали тот момент, в котором зловред вытаскивает токен от имени авторизованного пользователя. То есть авторизованному пользователю отдавать токен нельзя. А не авторизованному? Вооот! Единственный момент, когда мы безопасно можем отдать токен — это неавторизованному пользователю в момент авторизации, когда мы получаем от него логин и пароль. До этого момента он для нас никто, после этого момента (после выставления куки) — он для нас зловред.
А где хранить токен? Не в верстке и не в куках. LocalStorage вполне подойдет. Итого:

Решение #4
Генерим токен только при авторизации. Скриптом сразу кладем в localStorage, при каждом action добавляем токен в форму (можно повесить на обработчика submit)
Неограниченный круг лиц и доменов, рефереры не важны, спим спокойно. Годится для одностраничников, многостраничников, при генерации верстки на сервере, на клиенте. В общем серебряная пуля. Ок.
Довольный результатом, я зашел в redmine, оформил все заметками в вики и продолжил работу над проектом. Но мозг не унимался, осталась какая то незавершенность в вопросе.
Перечитав все несколько раз, глаз зацепился за фразу про то что токен — брат-близнец куки, просто хранится в другом месте. Ок. А что если он не брат-близнец, а клон куки? То есть такой момент. Отдавая токен серверу, мы ведь фактически подтверждаем не факт владения секретом (эту функцию выполняет кука) а факт управления процессом, то есть контроль над транспортом. Ведь фактически, если мы не будем генерить токен, а возьмем куку (на клиенте), скриптом вставим ее в передаваемые параметры — что изменится? НИЧЕГО! Зловред не сможет это повторить! Давайте заново. Зловред не может прочитать нашу куку, но может инициировать ее передачу серверу при запросе, передачу в хидере! Это важно. Но он не может прочитать эту куку и передать ее в параметре формы. А мы можем! Итого:

Решение #5
Токены не нужны (в смысле их генерация и валидация) На клиенте — перед каждым запросом читаем куку и добавляем ее в параметр запроса. На сервере — сравниваем куку с параметром запроса, если совпадают — дальнейшие проверки (факт авторизации, права и прочее). Но! В этом случае мы вынуждены отказаться от Http-only кук и открыть доступ на чтение куки javascript-у. XSS-фобам это может не понравится.

Что же, пораскинем. Можно не убирать Http-only, а отдавать сгенерированный session id при авторизации и в куку и в скрипт, который ее сразу положит в localStorage. А дальше — также как и в решении #5. Ок? Нет. Сохранение куки в localStorage равносильно отмене Http-only с точки зрения XSS-атаки.

Прикольно! А почему так получилось (о пятом решении)? Поразмышляв, пришел к выводу что это произошло по той причине, что мы сделали из токена полную копию сессионной куки по функционалу, различие лишь в транспорте. Ок. В пятом решении мы отказались от токена в пользу куки, оставив альтернативный транспорт. А если сделать наоборот — отказаться от куки? Тоже вариант. То есть:

Решение #6
Традиционной сессии нет (через http headers). Id сессии генерится при авторизации, скрипт на клиенте при получении id сразу кладет его в local storage и при каждом запросе к серверу добавляет к запросу. Этого достаточно как для поверки авторизации клиента так и для защиты от csrf. Но отказ от http кук не делает это решение более защищенным от XSS чем пятое решение.

Такие вот мысли.
За бортом остались:

  • обсуждение методов доступа (GET, POST, HEAD, экзотика). Я не использую GET, что бы какой нибудь умник не мог, запостив «картинку» с нужным урлом на посещаемом ресурсе нагрузить мои сервера. В остальном без разницы — все что можем сделать с XMLHTTPRequest мы — может сделать зловред.
  • протоколы доступа. http я для себя вычеркнул, пока https, поглядываю на wss, но пока внятных мыслей нет. Буду рад комментариям по этой теме.
  • Разница в правах на броузере клиента между добропорядочным кодом с нашего сервера, подгруженным в страницу зловреда через script src и изначально чужим кодом, делающим запрос к нашему серверу. Хотя да, надо бы тоже подробно посмотреть.

В общем для себя я выбрал первый и пятый варианты, но пока не переписываю код, жду когда остынет. Предлагаю обсудить.


ссылки по теме:


P.S. Если в чем не прав — не стесняйтесь, бейте по пальцам и учите разуму, я не обидчивый.

ссылка на оригинал статьи http://habrahabr.ru/post/257601/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *