Как сделать безопасным код сайта на Битрикс: шпаргалка по основным уязвимостям, часть 2

2e98ee1339961a53772fbd5e08ba13e9.jpeg

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

В предыдущей статье я рассказала о методах защиты от SQL-инъекций и XSS-атак. 

Сегодня разберём защиту от CSRF- и SSRF-атак.

CSRF (Cross Site Request Forgery)

В классическом понимании Cross-Site Request Forgery (подделка межсайтовых запросов) работает так:

  • Пользователь авторизуется на сайте (например, абстрактный cool-bank.com) и его браузер сохраняет сессионные куки.

  • Этого пользователя методами социальной инженерии завлекают на сайт злоумышленника (evil.com), на котором есть форма примерно такого вида:

...
  • При переходе на evil.com Javascript вызывает form.submit,  отправляя форму на cool-bank.com.

  • cool-bank.com проверяет куки, видит, что пользователь авторизован, и обрабатывает форму, отправляя деньги от имени пользователя.

Вариантов реализации этой атаки множество и во многом вектор определяется лишь фантазией хакера.

На эксплойтах останавливаться не будем (мы же порядочные люди :)) и поговорим про защиту.

Как защищаться

Есть два основных подхода и они прекрасно работают вместе: CSRF-токены и SameSite cookie.

Пройдёмся по ним.

CSRF-токены — это уникальное, секретное и непредсказуемое значение, которое генерируется на стороне сервера и отправляется клиенту.

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

У нас для этого есть замечательные функции:

  • bitrix_sessid

  • bitrix_sessid_post

  • bitrix_sessid_get

  • check_bitrix_sessid

  1. bitrix_sessid возвращает CSRF-токен (он же — идентификатор сессии). Выглядит это как MD5-хэш:

    echo bitrix_sessid(); 
    //feb8414592f24d96f6fd0c656e6ccd67

    Эта функция служебная и сама по себе нас мало интересует, но она используется под капотом у следующих функций.

  2. bitrix_sessid_post возвращает строку вида:

    ''

    где $varname по умолчанию имеет значение «sessid», но при необходимости можно передать любое другое.

  3. bitrix_sessid_get — делает то же самое, но для GET-запросов. Возвращает строку вида:

    echo bitrix_sessid_get();
    //sessid=feb8414592f24d96f6fd0c656e6ccd67

    Эта функция используется, если нужно сделать какое-то конфиденциальное действие через GET (например, выход из системы).  Достаточно сформировать ссылку таким образом: 'url?parameters&' . bitrix_sessid_get();.

  4. check_bitrix_sessid — проверяет токен, который мы могли передать функциями, описанными выше. Она проверяет, совпало ли значение bitrix_sessid с $varname (по умолчанию sessid) или заголовком X-Bitrix-Csrf-Token.

    return (
       $request[$varname] === bitrix_sessid() ||
       $request->getHeader('X-Bitrix-Csrf-Token') === bitrix_sessid()
    );

В общем виде для проверки корректности запроса достаточно добавить check_bitrix_sessid в условие:

if (check_bitrix_sessid())
{
   ...
}

То же самое касается ajax-запросов, в них тоже нужно включать токен. Делается это простым движением руки: BX.bitrix_sessid().

Важное замечание про GET-запросы:  в общем случае запросы на изменение состояния приложения должны выполняться методом POST. Говоря только о безопасности, передача токена методом GET означает его бесплатное раскрытие третьим лицам. Если всё-таки очень хочется использовать GET с токеном, тогда после обработки запроса необходимо сделать редирект на нужный адрес, но уже без CSRF-токена.

Важное замечание про HTML-формы:  В HTML-формах иногда можно очень просто нарушить форматирование (так называемыми HTML-инъекциями), что может привести к утечке CSRF-токена. Например:

...

Если в $injection попадёт строка "\">

То есть, всё, что идёт после внедрённой строки и до первой встреченной одинарной кавычки, станет частью ссылки на hacker.com. И браузер попытается загрузить «картинку» по этой ссылке. Чтобы такого не произошло, токен желательно располагать как можно выше в форме. В идеале — в самом начале:

...

Хотя при внедрении JavaScript-кода это не поможет, потому что с его помощью можно обращаться к любым элементам DOM, включая CSRF-токен, вне зависимости от их расположения на странице.

Итого. Безопасный запрос от клиента включает в себя CSRF-токен, а безопасная обработка на сервере начинается с проверки валидности этого токена. Всё не сложно и требует использования всего пары функций :)

Идём дальше.

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

Если нам нужно задать новую безопасную куку, то сделать это можно так:

setcookie('cookie_name', 'cookie_value', [
    ...
    'samesite' => 'Strict' 
]);

Здесь Strict — это самый строгий режим. Есть ещё Lax и None.

Strict запрещает передачу куки во всех межсайтовых взаимодействиях, даже при прямом переходе по ссылке.

Lax — сбалансированный режим, который позволяет передачу кук в GET-запросах, но запрещает в POST.

None — тут всё очевидно :)

И всё же это не панацея.

Почему нельзя просто задать всем кукам атрибут Strict? Потому что это может негативно сказаться на пользовательском опыте в межсайтовой функциональности. Например, если пользователь авторизуется на сайте, а затем перейдёт по ссылке с другого сайта, куки с SameSite=Strict не будут отправлены. В результате юзер окажется неавторизованным, даже если он только что вошёл в систему.

Почему нельзя просто задать всем кукам атрибут Lax? В общем-то можно. Chrome, например, так и делает, но есть и методы обхода этих ограничений. Например, можно попробовать совершить какое-то действие через GET-запрос, даже если задумывали мы его как POST. Можно обойти ограничения с помощью перенаправлений на клиентской стороне (актуально и для Strict) и много чего ещё можно сделать. Вот тут подробнее о некоторых методах обхода.

Владельцы сайтов по-разному подходят к вопросам баланса между безопасностью и удобством, и они имеют на это полное право. Нельзя выставить всем одинаковый Strict или Lax, так как требования к функциональности и безопасности варьируются в зависимости от типа сайта. Например, интернет-магазины могут предпочесть Lax для удобства пользователей, а банковские приложения — Strict для максимальной защиты. Также стоит учитывать, что в старых версиях браузеров (например, Chrome до версии 51) эти атрибуты не поддерживаются. А кроме того, старые версии PHP не умеют писать эти атрибуты, там требуется ручная настройка :)

Итог. Более экзотические способы защиты рассматривать не будем, потому что обойти их порой проще, чем написать. В подавляющем большинстве случаев установки и проверки токена более чем достаточно для обеспечения безопасности. Главное — не стесняться им пользоваться.

SSRF (Server-Side Request Forgery)

SSRF — она же серверная подделка запросов — это атака, которая позволяет отправлять запросы от имени сервера. Ну, например:

$http = new HttpClient();
print_r($http->get($_GET['uri']));

Здесь сервер напрямую обращается к произвольному адресу, да ещё и выводит ответ на экран. Такой код — находка для багхантера.

Чем это чревато

Прежде чем мы разберёмся, как защищаться от SSRF, давайте разберёмся, зачем от неё вообще защищаться.

SSRF часто служит подспорьем для других, более критичных атак. Эталонный пример — доступ к внутренним сервисам. На условном сервере условная служба работает на условном порте 1337, доступ к которому недоступен извне. Напрямую хитовать этот порт никто не может, однако если в пример выше передать https://127.0.0.1:1337 (вместо 127.0.0.1 любой адрес из локальной сети), то адрес будет доступен, так как обращаются к нему с самой машины. Конкретно в случае выше мы увидим даже ответ сервиса, но опасны и те кейсы, в которых сервер ничего не отдаёт пользователю, когда ходит по ссылкам. Это позволяет, например, провести сканирование локальной сети изнутри и узнать много нового о системе, на которую планируется организовать атаку. В самых запущенных случаях это может привести даже к выполнению произвольного кода.

Что делать

Атака представляет опасность, если злоумышленник может обращаться к адресам в локальной сети. Соответственно нам надо не допустить такой возможности. Для этого в HTTP-клиенте есть опция setPrivateIp(false), которая отключает отправку запросов к частным адресам (а если с true — то включает; дефолтное значение). Давайте посмотрим, как она работает:

$http = new HttpClient(); // Создаём объект HTTP-клиента
$http->setPrivateIp(false); // Запрещаем ему обращаться к приватным IP
$http->get($_GET['uri']); // А сюда попробуем отправить какой-нибудь локальный адрес

Опустим всё колдовство клиента и сразу перейдём к проверке, которая в данном случае сработает:

if (!$this->privateIp)
{
   $ip = IpAddress::createByUri($punyUri);
   if ($ip->isPrivate())
   {
      $this->addError('PRIVATE_IP', "Resolved IP is incorrect or private: {$ip->get()}");
      return false;
   }
   $this->effectiveIp = $ip;
}

...

public function isPrivate(): bool
{
   return (filter_var($this->ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6 | FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false);
}

Адрес фильтруется силами и флагами самого PHP. Функция isPrivate() делает вывод, что адрес приватный, и всё, дальше запрос не идёт. Вот так просто :)

Сам клиент при этом отлично справляется со всевозможными путями обхода вроде разных представлений IP, поэтому заботиться об этом нам не придётся.

Скачать картинку онлайн без вирусов

Ну и дополним примером, ответив на смежный с нашей темой вопрос: как безопасно загрузить картинку с удалённого хоста?

Для решения этой проблемы есть два пути: сложный и несложный :)

Вот сложный путь:

  • Берём клиента и вешаем на него флажок setPrivateIp(false):

    $http = new HttpClient();
    $http->setPrivateIp(false);
  • Пишем файл во временную директорию, чтобы он до проверки не был доступен по прямому запросу:

    $temp_file = CTempFile::GetFileName(\Bitrix\Main\Security\Random::getString(32).".".Bitrix\Main\IO\Path::getExtension($_POST['uri']));
    if(CheckDirPath($temp_file)){
       $request = $http->download($_POST['uri'], $temp_file); // в $temp_file будет случайное имя с оригинальным расширением
       if ($request){ 
       // Следующий шаг
       }
    }
  • Проверяем картинку на корректность:

    $file = CFile::MakeFileArray($temp_file);
    if ($file) // MakeFileArray может отдать false, если что-то пошло не так
    { 
       $res = CFile::CheckImageFile($file);
       if ($res !== null)
       {
          // Картинка имеет неверный формат/размер, обрабатываем ошибку
       }
    }
  • Проводим уже дальнейшие преобразования.

А вот несложный путь:

$file = CFile::MakeFileArray($_GET['uri']);
if ($file){
   $res = CFile::CheckImageFile($file);
   ...
}

CFile::MakeFileArrayвсё умеет самостоятельно! Если передать в этот метод ссылку, он безопасно сделает всё то, что описано выше. И не надо ничего выдумывать :)

Замечание напоследок: не забывайте, что иногда по ссылкам также ходят функции, в названии которых это явно не указано (например, file_get_contents и readfile).

На этом на сегодня всё! В следующей статье я рассмотрю еще три способа защиты от популярных уязвимостей — нормализацию путей, предотвращение небезопасной десериализации и криптоподпись. Сохраняйте пост, подписывайтесь и ждите продолжения!

© Habrahabr.ru