PHP

Поширені помилки безпеки в Laravel застосунках

Alex Alex 30 вересня
Поширені помилки безпеки в Laravel застосунках

У більшості випадків уразливості безпеки виникають тільки через недостатню поінформованість, а не через халатність. Хоча ми виявили, що більшість розробників дбають про безпеку, але іноді вони не розуміють, як конкретний шаблон коду може привести до уразливості, тому в електронній книзі ми вирішили поділитися найпоширенішими проблемами безпеки, які ми бачили під час допомоги різним стартапам в захисті своїх застосунків 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 тут .

Поради щодо профілактики:

  1. Найкраща профілактика - не використовувати дані, надані користувачем, для створення правила валідації;

  2. Якщо вам потрібно створити правило перевірки на основі наданих даних (ID в нашому прикладі), обов'язково приведіть або підтвердіть надане значення, перш ніж поміщати його в правило перевірки.

XSS (міжсайтовий скриптинг) в Laravel Blade

Про XSS атаки повідомлялося і вони використовувалося з 1990-х років, але все ж іноді ми бачимо випадки, коли розробники недооцінюють небезпеку атаки через факт, що він виконується в браузері, а не на сервері. Але це може бути дуже небезпечно, наприклад, XSS-атака в панелі адміністратора може дозволити зловмиснику виконати такий код:

Some text
<input onfocus='$.post("/admin/users", {name:"MaliciousUser", email:
"[email protected]", 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
&lt;script&gt;alert(&quot;xss&quot;);&lt;/script&gt;</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-атаку.

Поради щодо профілактики:

  1. По можливості уникайте виведення даних в html без екранування, наданих користувачем.

  2. Якщо ви знаєте, що в деяких випадках дані можуть містити 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!») запускається, коли користувач натискає посилання «Мій веб-сайт» у обох випадках ...

Поради щодо профілактики:

  1. Перевіряйте посилання, надані користувачем. У більшості випадків вам потрібно тільки дозволити http/https схеми;

  2. В якості додаткового рівня безпеки перед виведенням ви можете замінити будь-яке посилання, що не починаючи зі схеми 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" : "[email protected]",
    "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, ви можете реалізувати щось на зразок цього, безумовно потребують додаткових ресурсів:

Поширені помилки безпеки в Laravel застосунках

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 і за його межами, які повинні обробляється кодом програми.

Рідше, але, все таки можуть зустрічатися і такі уразливості, тому потрібно зрозуміти  принцип їх роботи, щоб правильно прикрити вразливі місця:

  1. Race Condition

  2. Server Side Request Forgery

  3. PHP Type Juggling

  4. Unrestricted File Upload

  5. Path Traversal

  6. Sensitive Data Exposure

  7. Server Side Template Injection

  8. Insecure Deserialization

  9. Session Fixation

  10. XML External Entity (XXE) Processing

  11. JWT Security

Джерело: Common security mistakes in Laravel applications

Коментарі (0)

    Ще немає коментарів

Щоб залишити коментар необхідно авторизуватися.

Війти / Зареєструватися