Введение
В большинстве случаев уязвимости безопасности возникают только из-за недостаточной осведомленности, а не из-за халатности. Хотя мы обнаружили, что большинство разработчиков заботятся о безопасности, но иногда они не понимают, как конкретный шаблон кода может привести к уязвимости, поэтому в электронной книге мы решили поделиться наиболее распространенными проблемами безопасности, которые мы видели во время помощи разным стартапам в защите своих приложений Laravel. С каждым примером атаки мы также покажем лучшие практики по защите вашего приложения от атак. Мы надеемся, что эта информация окажется полезной для вас и вашей команды разработчиков.
CyberPanda Team
SQL-инъекции
Laravel предоставляет надежный конструктор запросов (Query Builder) и Eloquent ORM. И, благодаря им, большинство запросов по-умолчанию защищены в приложениях, поэтому, например, запрос типа
Product::where('category_id', $request->get('categoryId'))->get();
будет автоматически защищен, так как Laravel переведет код в подготовленный оператор и выполнит.
Но разработчики обычно делают ошибки, полагая, что Laravel защищает от всех SQL-инъекций, хотя есть некоторые векторы атак, которые Laravel не может защитить.
Вот наиболее распространенные причины SQL-инъекций, которые мы видели в современных приложениях Laravel во время наших проверок безопасности.
1. SQL-инъекция через имя столбца
Первая распространенная ошибка, которую мы видим, заключается в том, что многие люди думают будто Laravel будет избегать любого параметра, переданного в Query Builder или Eloquent. Но, на самом деле, это не так безопасно передавать имена столбцов, управляемых пользователем, в конструктор запросов.
Вот предупреждение от официальной документации Laravel:
PDO does not support binding column names. Therefore, you should never allow user input to dictate the column names referenced by your queries, including "order by" columns, etc. If you must allow the user to select certain columns to query against, always validate the column names against a white-list of allowed columns.
Таким образом, следующий код будет уязвим для SQL-инъекции:
$categoryId = $request->get('categoryId');
$orderBy = $request->get('orderBy');
Product::query()
->where('category_id', $categoryId)
->orderBy($orderBy)
->get();
и если кто-то делает запрос со следующим значением параметра orderBy
http://example.com/users?orderBy=id->test"' ASC, IF((SELECT count(*)
FROM users ) < 10, SLEEP(20), SLEEP(0)) DESC -- "'
под капотом будет выполнен следующий запрос и мы получим успешную SQL-инъекцию:
select
*
from `users`
order by `id`->'$."test"' ASC,
IF((SELECT count(*) FROM users ) < 10, SLEEP(20), SLEEP(0))
DESC -- "'"' asc limit 26 offset 0
Важно отметить, что показанный вектор атаки исправлен в последней версии Laravel*, но, тем не менее, Laravel предупреждает разработчиков даже в свежей документации, чтобы они не передавали имена столбцов в Query Builder от пользователей без их добавления в белый список.
В общем, даже если нет возможности превратить настраиваемый столбец во внедренную SQL-инъекцию, мы по-прежнему не рекомендуем разрешать сортировку данных по любому столбцу, заданному пользователем, так как это может создать угрозу безопасности.
Рассмотрим пример, когда таблица «users» может иметь какой-то секретный столбец "secretAnswer", умный злоумышленник может вывести значение без необходимости SQL-инъекции.
2. SQL-инъекция с помощью правил валидации
Давайте посмотрим на следующий упрощенный код проверки
$id = $request->route('id');
$rules = [
'username' => 'required|unique:users,name,' . $id,
];
$validator = Validator::make($request->post(), $rules);
Поскольку Laravel использует здесь $id
для запроса этой и значение не экранируется, это позволит злоумышленнику выполнить SQL-инъекции. Мы рассмотрим эту атаку более подробно в "Инъекция правил валидатора" > "SQL-инъекция".
3. SQL-инъекция через необработанные запросы
Еще одна закономерность, о которой стоит упомянуть, но менее распространенная, которую мы видим в нашей системе безопасности, проверки кода просто используют функцию DB::raw
в старом стиле и не экранируют переданные данные. Подобный паттерн обычно случается редко, в основном в тех случаях, когда есть необходимость пройти какой-то индивидуальный запрос. Если вам нужно использовать функцию DB::raw
для какого-то специального запроса, убедитесь, что вы избегаете переданных данных через метод DB::getPdo()->quote
.
Инъекция правил валидатора
Давайте посмотрим на следующий уязвимый код:
public function update(Request $request) {
$id = $request->route('id');
$rules = [
'username' => 'required|unique:users,username,' . $id,
];
$validator = Validator::make($request->post(), $rules);
if ($validator->fails()) {
return response()->json($validator->errors(), 422);
}
$user = User::findOrFail($id);
$user->fill($validator->validated());
$user->save();
return response()->json(['user' => $user]);
}
Вы заметили уязвимость в строке required|unique:users,username,'. $id
? Так держать! (нет)
Итак, здесь правило unique
обеспечивает уникальность имени пользователя внутри таблица пользователей, и оно также проигнорирует строку с данным идентификатором во время проверки. Но проблема в том, что мы получили значение $id
из запроса и, не проверяя его, использовали его для проверки на основе ввода пользователя. Таким образом, используя это, мы можем настроить правила проверки и создания векторов атак, давайте рассмотрим следующие примеры.
1. Сделать правило проверки необязательным
Самое простое, что мы можем сделать здесь - это отправить запрос с ID = 10|sometimes
, который изменит правило проверки на required|unique:users,username,10|sometimes
и позволит нам не пропускать имя пользователя в данных запроса, в зависимости от бизнес-логики вашего приложения, это может создать проблему безопасности.
2. DDOS сервер путем создания злого правила проверки REGEX
Другой вектор атаки может заключаться в создании злой и уязвимой проверки Regex для ReDoS атаки и DDOS приложения. Например, следующий запрос потребляет много процессорного времени, и если несколько запросов отправляются одновременно, это может вызвать большой скачок нагрузки на ЦП сервера:
PUT /api/users/1,id,name,444|regex:%23(.*a){100}%23
{
"username": "aaaaa.....ALOT_OF_REPETED_As_aaaaaaaaaa"
}
3. SQL-инъекция
Простейшей SQL-инъекцией было бы просто добавить дополнительное правило проверки передаваемое в запрос. Например:
PUT /api/users/1,id,name,444|unique:users,secret_col_name_here
{
"username": "secret_value_to_check"
}
Важно упомянуть, поскольку с помощью unique
мы можем предоставить как настраиваемый столбец имени и значения (значения не проходят через привязку параметров PDO) возможность SQL-инъекции здесь не может быть ограничена просто упомянутым выше вектором атаки. Для получения дополнительных сведений ознакомьтесь с публикацией блога Laravel здесь.
Советы по профилактике:
Лучшая профилактика - не использовать данные, предоставленные пользователем, для создания правила валидации;
Если вам нужно создать правило проверки на основе предоставленных данных (ID в нашем примере), обязательно приведите или подтвердите предоставленное значение, прежде чем помещать его в правило проверки.
XSS (межсайтовый скриптинг) в Laravel Blade
Об атаках XSS сообщалось и использовалось с 1990-х годов, но всё же иногда мы видим случаи, когда разработчики недооценивают опасность атаки из-за того факта, что он выполняется в браузере, а не на сервере. Но это может быть очень опасно, например, XSS-атака в панели администратора может позволить злоумышленнику выполнить такой код:
Some text
<input onfocus='$.post("/admin/users", {name:"MaliciousUser", email:
"MaliciousUser@example.com", password: "test123", });' autofocus />
test
Это позволит злоумышленнику создать пользователя-администратора с его учетными данными и взять на себя права администратора. И даже ограничение админки IP-адресами не предотвратит атаку, поскольку код будет выполнен в браузере пользователя, имеющего доступ к сети/приложение.
Теперь давайте посмотрим, каковы возможные векторы XSS-атак в Laravel.
Если вы используете Laravel Blade, он защищает вас от большинства XSS-атак, например, такая атака не сработает:
// $name = 'John Doe <script>alert("xss");</script>';
<div class="user-card">
<div> ... </div>
<div>{{ $name }}</div>
<div> ... </div>
</div>
потому что оператор Blade {{ }}
автоматически кодирует вывод. Итак, сервер отправит в браузер следующий правильно закодированный код:
<div class="user-card">
<div> ... </div>
<div>John Doe
<script>alert("xss");</script></div>
<div> ... </div>
</div>
что предотвратит атаку XSS. Но не всё Laravel (или любой другой фреймворк) может обработать за вас, вот несколько примеров XSS-атак, которые мы обнаружили наиболее распространенными во время наших проверок безопасности:
1. XSS через объявление {!! $userBio !!}
Иногда вам нужно вывести текст, содержащий HTML, и для этого вы будете использовать {!! !!}
:
// $userBio = 'Hi, I am John Doe <script>alert("xss");</script>';
<div class="user-card">
<div> ... </div>
<div>{!! $userBio !!}</div>
<div> ... </div>
</div>
В этом случае Laravel ничего не может сделать за вас, и если $userBio
содержит JavaScript код, он будет выполнен как есть и мы получим XSS-атаку.
Советы по профилактике:
По возможности избегайте вывода данных в html без экранирования, предоставленных пользователем.
Если вы знаете, что в некоторых случаях данные могут содержать HTML, используйте такой пакет, как htmlpurifier.org, чтобы очистить HTML от JS и нежелательных тегов перед выводом контента.
2. XSS через атрибут a.href
Если вы выводите значение, указанное пользователем, в виде ссылки, покажем несколько примеров того, как это может превратиться в XSS-атаку:
Пример 1: Использование javascript:code
// $userWebsite = "javascript:alert('Hacked!');";
<a href="{!! $userWebsite !!}" >My Website</a>
Пример 2: Использование данных в кодировке base64:
Обратите внимание, что этот будет работать только для фреймов не верхнего уровня.
// $userWebsite =
"data:text/html;base64,PHNjcmlwdD5hbGVydCgiSGFja2VkISIpOzwvc2NyaXB0Pg
==";
<a href="{!! $userWebsite !!}" >My Website</a>
код предупреждения («Hacked!») запускается, когда пользователь нажимает ссылку «Мой веб-сайт» в обоих случаях...
Советы по профилактике:
Проверяйте ссылки, предоставленные пользователем. В большинстве случаев вам нужно только разрешить http/https схемы;
В качестве дополнительного уровня безопасности перед выводом вы можете заменить любую ссылку, которая не начиная со схемы http/https значением «#broken-link».
3. XSS через кастомную директиву
Если в вашем Blade коде есть настраиваемые директивы, вам нужно вручную избежать любого вывода, который не должен отображаться в коде как HTML. Например, следующиая пользовательская директива уязвима, потому что переменная имени не закодирована, а Laravel ничего не может с этим поделать:
// Registering the directive code
Blade::directive('hello', function ($name) {
return "<?php echo 'Hello ' . $name; ?>";
});
// user.blade.php file
// $name = 'John Doe <script>alert("xss");</script>';
@hello($name);
Советы по профилактике:
Используйте функцию Laravel e()
, чтобы избежать любого кода, предоставленного пользователем. Вышеупомянутые 3 уязвимости являются наиболее распространенными, которые мы видели в разных приложениях Laravel во время наших проверок. Как видите, в этом разделе мы собрали некоторые XSS-атаки в рамках Laravel, но чтобы полностью предотвратить XSS-атаки, вам также необходимо убедиться, что ваш интерфейсный код, который может быть React.js, Vue.js, ванильным javascript или старомодным jQuery, также защищает от XSS-атак.
Уязвимости массового назначения в Laravel
Eloquent, как и многие другие ORM, имеет приятную особенность, позволяющую назначать свойства объекту без необходимости присваивать каждое значение индивидуально. Это хорошая функция, сохраняющая много времени и строк кода, но при неправильном использовании может привести к уязвимости.
Например, вот упрощённый пример небезопасного кода, который мы обнаружили во время проверки кода одного из наших клиентов:
// app/Models/User.php file
class User extends Authenticatable
{
use SoftDeletes;
const ROLE_USER = 'user';
const ROLE_ADMINISTRATOR = 'administrator';
protected $fillable = ['name', 'email', 'password', 'role'];
// ... rest of the code ...
}
// app/Http/Requests/StoreUserRequest.php file
class StoreUserRequest extends Request
{
public function rules()
{
return [
'name' => 'string|required',
'email' => 'email|required',
'password' => 'string|required|min:6',
'confirm_password' => 'same:password',
];
}
}
// app/Controllers/UserController.php file
class UserController extends Controller
{
public function store(StoreUserRequest $request)
{
$user = new User();
$user->role = User::ROLE_USER;
$user->fill($request->all());
$user->save();
return response()->json([
'success' => true,
],201);
}
// ... rest of the code ...
}
Проблема здесь в том, что если злоумышленник отправит полезную нагрузку ниже, он сможет зарегистрировать пользователя-администратора с более высокими привилегиями и, возможно, возьмёт на себя админку приложения.
{
"name" : "Hacker",
"email" : "hacker@example.com",
"role" : "administrator",
"password" : "some_random_password",
"confirm_password" : "some_random_password"
}
Вы можете задаться вопросом почему "role" находится в атрибуте $fillable
. Если бы поле "role" не было указано в переменной, то и проблем бы не было. Причина, по которой оно было добавлено, в том, что существует ещё один метод API, позволяющий управлять ролью пользователей - это общий шаблон, который мы видим в приложениях Laravel, проверяем и тестируем на инъекции. Поле $fillable
отлично подходит, если у вас простое приложение, но им становится трудно управлять, когда приложение растёт и появляются несколько методов API, работающими с ролями ACL.
Вот несколько советов о том, как предотвратить уязвимости массового назначения в Laravel.
Советы по профилактике:
1. Передавать модели только проверенные поля
Это, вероятно, наиболее эффективный метод борьбы с массовыми атаками. Вместо передачи всех данных из запроса вы можете передавать только те поля, которые были указаны. В приведенном выше примере кода это будут "name", "email" и "password". Для этого Laravel предоставляет вам метод $request->validated()
, возвращающий только проверенные поля.
Итак, в приведенном выше коде замените $request->all()
на $request->validated()
и это устранит проблему:
public function store(StoreUserRequest $request)
{
$user = new User();
$user->role = User::ROLE_USER;
$user->fill($request->validated());
$user->save();
return response()->json([
'success' => true,
],201);
}
Если вы не используете проверку запросов Laravel, вы также можете использовать $request->validate()
или $validator->validated()
, который также возвращает только проверенные данные.
Это хорошая практика, поскольку она не только защищает вас от массовых назначений, но и предотвращает сохранение свойств, для которых мы забыли добавить проверку.
2. Используйте белый список вместо черного
Мы рекомендуем использовать $fillable
(который вносит в белый список только столбцы, которые могут быть массовыми назначено) вместо $guarded
(определяет свойства, которые не могут быть назначены массово), потому что вы можете легко забыть добавить новый столбец в массив $guarded
, оставив его открытым для массового использования по умолчанию.
3. . С осторожностью используйте метод $model->forceFill($data)
Метод $model->forceFill
игнорирует всю конфигурацию, установленную в $forceFill
и $fillable
и сохраняет переданные данные в модели. Если вам нужно использовать forceFill
, убедитесь, что переданные данные не могут быть изменены пользователем.
Отсутствие защиты от атак с использованием учетных данных
Итак, прежде всего, что такое атака с заполнением учетных данных? Это тип брутфорс атаки, где атаки отправляют пары имени пользователя и пароля, полученные в результате утечки других данных. Таким образом, например, злоумышленник получит миллионы логинов/паролей из утечек данных и автоматически попробует авторизоваться на вебсайте. Так как многие люди используют одно и то же сочетание имени пользователя и пароля на разных сайтах, согласно отчетам, в среднем, около 0,1-0,2% попыток будут успешными. Итак, если у веб-сайта около 1 миллиона пользователей, а злоумышленник использует 10 млн пар имен пользователей и паролей от других утечек данных, взломано будет примерно 10'000-20'000 аккаунтов. Чем больше база злоумышленника, тем потенциально взломанных учётных записей.
Заполнение учетных данных - один из самых популярных способов атак, так как он требует лишь небольшого усилия со стороны хакера.
По-умолчанию Laravel Auth имеет базовую защиту от брутфорса, где проверяет, сколько запросов поступают с одного и того же IP-адреса для одного и того же пользователя и будут блокировать его, но нет защиты от вброса учетных данных. Вам нужно будет реализовать защиту самим, и вот несколько советов для вас:
1. Внедрить многофакторную аутентификацию
Вероятно, это одна из самых эффективных мер по улучшению безопасности вашего приложения. Symantec считает, что до 80% утечек данных могут можно предотвратить путем внедрения 2FA.
2. Ограничить количество запросов для входа в конечную точку с одного IP-адреса
Это не исключит атаку, если злоумышленник имеет доступ к широкому спектру IP, но определенно усложнит.
3. Добавить капчу в конечные точки аутентификации
Подобно блокировке IP, не будет обеспечена 100% защита, но опять же улучшится безопасность.
4. Обнаруживать, когда пользователь использует имя пользователя/пароль от других утечек данных
Если ваша компания типа FaceBook, вы можете реализовать что-то вроде этого, определенно потребует дополнительных ресурсов:
5. Внедрить надежную систему мониторинга и оповещения
Внедрите надежную систему мониторинга и оповещения, которая предупредит вас о подозрительном трафике, поступающем на конечные точки аутентификации.
Сломанный контроль доступа
Хотя хороший фреймворк, такой как Laravel, может предоставить вам большую защиту из коробки от атак типа SQL-инъекций и XSS, он не может защитить вас от обхода контроля доступа атаки, поскольку логика ACL должна жить в коде приложения. Вот почему ACL обход - одна из самых распространенных проблем, которые мы видим в современных приложениях.
Laravel предоставляет гейты и политики, которые вы можете использовать для ограничения доступа в зависимости от роли пользователя.
Отсутствуют заголовки безопасности HTTP
Заголовки безопасности - это заголовки ответов HTTP (HTTP Strict Transport Security, X-Frame-Options, X-Content-Type-Options, X-XSS-Protection, Политика безопасности контента, и т. д.), которые ваше приложение может использовать для повышения безопасности. После установки эти HTTP заголовки ответов могут ограничить запуск современных браузеров в легко предотвратимые уязвимости. Например, заголовок HTTP Strict Transport Security заставит приложение всегда использовать HTTPS и предотвратит атаки посредника (обратите внимание, что перенаправления HTTP на HTTPS недостаточно для предотвращения атак посредника). Или установка правильных X-Frame-Options предотвратит атаки кликджекинга.
Во время наших тестов на проникновение мы все еще видим много приложений Laravel, в которых отсутствуют или неправильно настроены заголовки безопасности. И здесь вы тоже можете задаться вопросом, раз заголовки безопасности так важны, почему они не включены по умолчанию в браузерах? Причина в том, что, если браузеры включают эти настройки, они нарушат работу многих устаревших приложений, и это единственная причина, по которой браузеры оставляют разработчикам выбор.
По умолчанию Laravel не имеет заголовков, поэтому вам нужно будет реализовать себя. И вот несколько ресурсов, которые могут быть полезны:
Не отслеживание уязвимых пакетов в приложении
Каждый год в открытом исходном коде сообщается о десятках тысяч новых уязвимостей. И, например, одна из таких уязвимостей обошлась в размере 700 миллионов долларов штрафа и вреда репутации Equifax. Хакеры обнаружили, что один из серверов Equifax содержал уязвимую версию Apache Struts, о которой сообщалось 2 месяца назад но не было исправлено на сервере. Они использовали уязвимость для проникновения на серверы Equifax и украли личные записи 147,9 миллиона американцев.
С этой точки зрения приложения Laravel ничем не отличаются от других приложений, и важно внедрить надлежащий инструмент мониторинга и оповещения, который будет отслеживать все пакеты, которые используются в вашем коде и будут предупреждать вас о новой уязвимости до того, как дыры безопасности будут обнаружены и приложение подвергнется взлому.
Вывод
Хотя Laravel один из самых безопасных веб-фреймворков по дизайну, безопасность веб-приложения - это общая ответственность. Такой фреймворк, как Laravel, предоставит вам высокий API уровня (например, Eloquent), который по умолчанию устраняет множество проблем с безопасностью, но есть несколько векторов атак внутри Laravel и за его пределами, которые должны обрабатывается кодом приложения.
Реже, но, всё же, могут встречаться и следующие зависимости в зависимости от набора функциий и нужно понять их принцип работы, чтобы правильно прикрыть уязвимые места приложения:
Примечание от переводчика
В некоторых примерах оригинальной статьи неправильно указан код уязвимостей. Например, {{$userBio}}
вместо {!!$userBio!!}
.
* "вектор атаки исправлен в последней версии Laravel" - по оригинальной статье не понятна дата её выхода, как и по их сайту, вследствие чего не удалось установить какая именно версия Laravel была на тот момент последней. Тем не менее, статья попала в руки в день перевода, и на этот момент пара дней как существовала Laravel 8.