Как стать автором
Обновить
192.33

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

Время на прочтение7 мин
Количество просмотров4.2K

Привет! Меня зовут Анастасия Соколенко, я отвечаю за безопасную разработку в Битрикс24. Вместе с коллегами мы не только проверяем код, который пишут наши разработчики, но и учим их делать его максимально безопасным.

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

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

SQL-инъекции и XSS-атаки входят в топ уязвимостей по мнению экспертов в области ИБ. Лаборатория Касперского ставит их на 4 и 5 места в своем рейтинге. С них мы и начнём.

Экранирование: защищаемся от XSS-атак

Злоумышленники могут писать плохие вещи туда, куда, по-хорошему, писать бы только хорошее. 

Например, они могут отправить через форму на сайте не требуемые данные, а вредоносный скрипт. 

Все данные, которые мы получаем, либо отдаём пользователю в HTML или JavaScript, надо делать безопасными. Это значит, что все спецсимволы должны быть заменены HTML-аналогами, как при считывании данных из форм, так и при их выводе на страницу сайта.

Есть несколько функций, расскажу, как правильно их использовать и как нельзя делать.

Функция htmlspecialcharsbx

Также можно использовать метод \Bitrix\Main\Text\HtmlFilter::encode.

Экранирует обычный текст перед выводом в HTML.

Вот пример, как надо:

<div><?=htmlspecialcharsbx($foo)?></div>
<textarea><?=htmlspecialcharsbx($foo)?></textarea>

И как не надо:

<div><?=$foo?></div>
<textarea><?=$foo?></textarea>

При этом значения HTML-атрибутов должны заключаться в двойные кавычки:

<input type="text" name="foo" value="<?=htmlspecialcharsbx($fooValue)?>" />
<select name="<?=htmlspecialcharsbx($name)?>" id="<?=htmlspecialcharsbx($id)?>">

С одиночными кавычками фокус не пройдёт и функция ничего не сделает. Так делать не надо:

<input type="text" name="foo" value='<?=htmlspecialcharsbx($fooValue)?>' />

CUtil::JSEscape

Экранирует строки, которые выводятся в JavaScript. Подходит для строк, заключённых в кавычки.

Вот так надо:

<script>
var foo = '<?=CUtil::JSEscape($foo)?>';
</script>

А вот так не надо (нет кавычек, это не строковый литерал):

<script>
var foo = <?=CUtil::JSEscape($foo)?>;
</script>

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

Json::encode

Эта функция помогает выводить JS-объекты и JSON-строки:

<?$foo = ['key' => 'value'];?>
<script>
var foo = <?=Bitrix\Main\Web\Json::encode($foo)?>;
</script>

CBXSanitizer

Если по какой-то причине нужно вывести HTML-код от пользователя, следует использовать класс CBXSanitizer. Его метод sanitizeHtml преобразует весь полученный HTML-текст.

То есть, делаем вот так:

<div><?=(new CBXSanitizer)->sanitizeHtml($foo);?></div>

Эта штука оставит в тексте только разрешённые теги (их даже можно настраивать, подробнее - в документации).

Пара замечаний

  • Можно не экранировать только текст, который никак не может быть изменён пользователем: явно заданные в коде строки (не относится к языковым фразам), автоинкрементные значения в БД и  т. д.

  • У нас есть функция htmlspecialcharsEx, использовать её нежелательно. Ею можно воспользоваться только для вывода между тегами, когда нужно показать HTML-entity (она работает на основании чёрного списка и учитывает не все возможные варианты)

$foo = '&apos;-prompt(/XSS/)-&#x00000000027';
$someData = CUtil::JSescape($foo);
$someData = htmlspecialcharsEx($someData);
?>

<script>
	function doSome(message) {
		console.log(message);
	}
</script>

<a href="#" onclick="doSome('<?=$someData?>');">Do something!</a>
  • Если нужно экранировать HTML и JS одновременно (например, onclick), эскейпим двумя функциями:

<?
$someData = CUtil::JSescape($foo);
$someData = htmlspecialcharsbx($someData);
?>
<a href="#" onclick="doSome('<?=$someData?>');">Do something amazing!</a>

SQL инъекции

Все мы знаем, что такое SQL-инъекция и что так делать нельзя:
SELECT * FROM b_user WHERE id=$_REQUEST['id'];

Но как тогда можно? Давайте разбираться.

К работе с базами данных у нас есть два подхода: 

  1. Прямые запросы (CDatabase и Application::getConnection) .

  2. ORM.

Прямые запросы

CDatabase

Она же $DB. Это класс для прямого взаимодействия с БД. В классе реализованы методы на все случаи жизни, но просто бездумное их использование нас не обезопасит.

Ключ к безопасности SQL-запросов — это приведение типов, экранирование и подготовленные выражения.

Первое — самое простое. Если в параметре должно быть целое число, приводим его к соответствующему типу. Пример выше сразу станет безопасным, если всего лишь  поменять его таким образом:

$id = intval($_REQUEST['id']);
$res = $DB->Query("SELECT * FROM b_user WHERE id=$id");

Второе немногим сложнее. Если из запроса мы ждём строку, тогда нельзя позволить злоумышленнику выйти за пределы наших кавычек. В CDatabase есть метод ForSql, который этим и занимается. Результат его работы обязательно должен ставиться внутрь кавычек, иначе мы ни от чего не защитимся. Так абсолютно небезопасный запрос:

$login = $_REQUEST['login'];
$res = $DB->Query("SELECT * FROM b_user WHERE LOGIN='$login'");

Становится абсолютно безопасным:

$login = $DB->ForSql($_REQUEST['login']);
$res = $DB->Query("SELECT * FROM b_user WHERE LOGIN='$login'");

И, наконец, третье, самое универсальное. Для подготовленных выражений в CDatabase есть методы PrepareInsert и PrepareUpdate. Если мы хотим добавить или изменить записи в БД, тогда подготовленные выражения — то, что нам нужно. Под капотом этих функций происходят все необходимые преобразования входных данных.

Код ниже абсолютно безопасно вставляет в таблицу пользовательский ввод:

$arInsert = $DB->PrepareInsert("b_user", ["LOGIN" => $_REQUEST["login"]]);
$sql = "INSERT INTO b_user (".$arInsert[0].") VALUES (".$arInsert[1].")";
$res = $DB->Query($sql);

Замечания:

В функциях AddPrepareInsertPrepareUpdate и PrepareUpdateBind есть вот такой код:

if(array_key_exists("~".$strColumnName, $arFields))
{
   $strInsert1 .= ", `".$strColumnName."`";
   $strInsert2 .= ", ".$arFields["~".$strColumnName];
}

и

if(is_set($arFields, "~".$strColumnName))
{
   $strUpdate .= ", $strTableAlias`".$strColumnName."` = ".$arFields["~".$strColumnName];
}

Это значит, что если в $arFields какое-то поле начинается с ~, то оно попадёт в запрос без каких-либо преобразований (было поле ~field, а попадёт без обработки в поле field). Поэтому туда не должны передаваться непроверенные ключи.

Application::getConnection

Эта функция вернёт объект MysqliConnection для выполнения запросов к базе, а для очистки запросов внутри неё есть функция getSqlHelper().

У объекта хелпера есть аналогичная функция forSql:

$connection = \Bitrix\Main\Application::getConnection();
$helper = $connection->getSqlHelper();
$login = $helper->forSql($_GET['login']);
$res = $connection->query("SELECT * FROM b_user where LOGIN='$login'");

И аналогичные функции prepareInsert и prepareUpdate. Здесь актуальны все советы из предыдущего пункта.

ORM

ORM прекрасен не только удобством, но и безопасностью. С его помощью в большинстве случаев читать и записывать можно даже неэкранированные пользовательские данные. Но не забывайте про XSS-атаки, которые мы рассмотрели выше — в БД может попасть HTML/JS код. Также важно учитывать уязвимости, такие как IDOR, которые могут позволить пользователю получить к данным из БД доступ, которого у него быть не должно, из-за недостаточной проверки прав.

Однако и ORM-инъекции — это не миф.

Как это работает у нас:

При чтении данных из БД (метод getList) можно указать select и filter. И оба эти параметра уязвимы, если получают на вход пользовательские данные. В этом случае можно вытащить или сбрутить некоторые записи в текущей таблице, а если есть поле ReferenceField, то ещё и из связанных.

Select

Select можно задать прямо в getList:

$result = \Bitrix\Landing\Internals\LandingTable::getList([
    'select' => $some_select
]);

Или с помощью setSelect:

$query = WorkgroupTable::query();
$query->setSelect($select);

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

$select = $_REQUEST['select']
//['ID' => 'ID', 'TITLE' => 'TITLE', 'LAST_NAME' => 'RESPONSIBLE.LAST_NAME'];

$result = \Bitrix\Tasks\Internals\TaskTable::getList([
	'select' => $select
]);
var_dump($result->fetchAll());

/*  ...
    array(3) {
    ["ID"]=>
    string(1) "1"
    ["TITLE"]=>
    string(4) "Task Title"
    ["LAST_NAME"]=>
    string(20) "Lastname"
    ...
  }
*/

Filter

С фильтром ситуация аналогичная: при наличии ReferenceField можно достать что-то из связанных таблиц, только выглядеть это будет как слепая инъекция. Например:

$result = \Bitrix\Main\UserTable::getList([
	'filter' => $some_filter
]);

Если в фильтр попадает ['ID' => 1, '%=EMAIL' => "A%"] и что-то возвращается, значит email начинается с буквы А. Если нет - пробуем другой символ. Немного времени с терпением и можно сдампить всё, что плохо лежит (пример абстрактный, подставьте нужное поле).

Решение аналогичное: никаких пользовательских массивов сюда попадать не должно, а входные данные должны проходить проверку.

Замечания:

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

  2. Есть у нас такой класс \Bitrix\Main\DB\SqlExpression, нужный для определения пользовательских SQL-запросов в фильтрах ORM. Если там будет хотя бы один непроверенный параметр, получится очередная инъекция. Например:

$filterIds = [1, 2, 3, $_GET['id']];

//$_GET['id'] = (select * from b_user where login="admin")

$filterList = \Bitrix\Disk\Internals\VolumeTable::getList([
   'filter' => [
      '@ID' => new \Bitrix\Main\DB\SqlExpression(implode(', ', $filterIds)),
   ],
]);

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

Аналогично с \Bitrix\Main\Entity\ExpressionField. Это тоже будет инъекцией:

$filterList = \Bitrix\Disk\Internals\VolumeTable::getList([
   'runtime' => array(
      new \Bitrix\Main\Entity\ExpressionField('CNT', $_GET['count'])
   )
]);

Следовательно, сюда тоже нельзя передавать непроверенный пользовательский ввод.

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

Теги:
Хабы:
Всего голосов 19: ↑18 и ↓1+19
Комментарии3

Публикации

Информация

Сайт
www.bitrix24.ru
Дата регистрации
Дата основания
1998
Численность
501–1 000 человек
Местоположение
Россия