• Час читання ~2 хв
  • 25.06.2022

Основна мета розробника – завоювати довіру наших користувачів. Ми хочемо, щоб вони довіряли нашому коду, нашим додаткам і нашому бренду. Тому що якщо користувач довіряє нам, він буде повертатися. Вони приймуть і живуть з дратівливими помилками та відсутніми функціями (не те, щоб ми цього хотіли, але вони трапляються...), і вони будуть вірити, що все інше працюватиме, що їхні дані будуть у безпеці, а ми не витрачаємо їх час.

Однак, як розробник, ми абсолютно не можемо довіряти нашим користувачам. Завжди!

Коли ми говоримо про довіру користувачів, це означає, що ми не можемо довіряти їхній інформації. Ми не можемо довіряти їм надавати правильну інформацію, використовувати правильні формати чи навіть виконувати лише очікувані дії.Проблема полягає в тому, що користувачі — складні створіння, що розчаровують. Деякі користувачі мають злі наміри і будуть шукати способи зламати або компрометувати ваші програми. Інші можуть думати інакше, ніж ви, і робитимуть те, чого ви не очікуєте – натикаючись на помилки та доступ до даних, до яких вони не повинні мати доступу (моя дружина – експерт у цьому!).А інші просто розгублені й розгублені, і намагатимуться повернутися до того, з чого почали.

Тож наше завдання як розробників – захистити наші програми від наших користувачів. Нам потрібно бути параноїком щодо введення, яке ми отримуємо, і мати кілька засобів захисту, щоб гарантувати, що незалежно від того, що робить користувач, наша програма безпечна. Ну і якось переконати наших користувачів, що ми їм не довіряємо, щоб вони довіряли нам.Легко, правда?

Сьогодні ми розглянемо кілька способів уникнути довіри своїм користувачам:

Перевірка введених даних

Коли ви думаєте про "введення" у свою програму, ви відразу думаєте про форми та дані форми. Ви просите своїх користувачів надати деяку інформацію або відповісти на деякі запитання, а потім отримуєте та зберігаєте вхідні дані, які вони надали.

Розгляньте таку форму:

Основна форма

Зі зібраної тут інформації було б дуже просто припустити, що адреса електронної пошти буде дійсною адресою електронної пошти (особливо якщо ми використовували поле введення електронної пошти HTML5), а країна буде дійсною (оскільки це поле вибору).

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

Замість цього нам потрібно чітко пояснити нашу перевірку введених даних:

  • First / last name
    • Required field
    • Must be a string
    • Max length 255
  • Email address
    • Required field
    • Must be a string
    • Must be a recognisable email address format
    • Max length 255
    • Must not be used by another user in the database
  • Country
    • Required field
    • Must be in a pre-defined list of allowed countries

Тепер, коли ми визначили наші чіткі правила, ми можемо використовувати неймовірно потужний компонент перевірки для перевірки введених даних, щоб переконатися, що це саме те, що ми очікуємо.

$data = $request->validate([
    'first_name' => ['required', 'string', 'max:255'],
    'Last_name'  => ['required', 'string', 'max:255'],
    'email'      => ['required', 'string', 'email', 'max:255', 'unique:users'],
    'country'    => ['required', Rule::in($this->allowedCountries())],
]);

Якщо перевірка пройдена, ми знаємо, що $data містить значення, які відповідають нашим очікуванням, які можна безпечно зберігати в базі даних та обережно використовувати в усій програмі.Країна безпечна у використанні, оскільки вона буде точно відповідати нашому списку дозволених, і ми знаємо, що електронний лист має дійсний формат, тому ми можемо почати надсилати сповіщення на нього.

З іншого боку, ми перейдемо до імені/прізвища.

Перш ніж ми перейдемо далі, важливо зауважити, що валідатор повертає лише ключі, які були включені в правила перевірки. Це означає, що будь-які додаткові дані, які в кінцевому підсумку будуть надіслані у форму, будуть ігноруватися, що дозволить безпечно передавати їх безпосередньо в модель за допомогою create(), fill() або update(), уникаючи того, що відомо як уразливість масового призначення. Так я завжди зберігаю дані про свої моделі.

Параметризовані запити

Отже, ми впровадили наші форми та запитали у користувачів дані, які вони надали. Але тепер нам потрібно написати деякі запити до бази даних, щоб використовувати наші дані. Знову виникає ця неприємна проблема довіри.

У Laravel ми зазвичай пишемо наші запити так:

$name = $request->query(‘name');
$user = DB::table('users')->where('name', $name)->get();

Ключовим компонентом тут є метод where(), де другий параметр ($name) надходить безпосередньо з введення користувача. Laravel автоматично включає цей другий параметр як параметризований запит, що запобігає ін’єкції SQL, передаючи його безпосередньо в базу даних.Це одна з блискучих функцій Laravel, яка ускладнює ненавмисне написання вразливих запитів.

Однак, що робити, якщо вам потрібен справді складний запит або ви хочете використати специфічну логіку системи баз даних? Або якщо вам просто потрібно виконати запит, який набагато ефективніше писати в необробленому SQL?

Ви побачите, що іноді вам потрібно зануритися в спеціальні запити, і саме тоді параметризовані запити важливі.

Розгляньте цей код:

public function store(Request $request)
{
    $data = $request->validate([
        'game' => ['required'],
        'date' => ['required'],
    ])
 
    DB::statement("UPDATE game_user SET turns += event_increment WHERE game_id = {$data['game']}");
}

Коли розробник писав цей код, вони очікували, що $data['game'] буде цілим числом, коли воно буде введено в запит (і вони прочитали це необроблені цілі порівняння швидше!).Звичайно, це передано через браузер, але вони вирішили, що це приховане поле, і ніхто не помітить! Але це все ще поле введення, і хакер може змінювати його скільки завгодно...

Як я згадував раніше, Laravel включає параметризацію, вбудовану в конструктор запитів, і ми можемо легко використовувати це під час створення запитів вручну.Замість того, щоб безпосередньо вводити змінні в рядок запиту, замініть їх знаком питання (?) і включіть їх як другий параметр у виклик методу. База даних розуміє, що знак питання є заповнювачем, і знає, як безпечно замінити параметри в запиті, коли він виконується.

У цьому випадку ми можемо зробити щось подібне:

DB::statement("UPDATE game_user SET turns += event_increment WHERE game_id = ?", [$data['game']]);

Загалом усі методи запиту Laravel підтримують параметри таким чином, тому немає виправдання не використовувати їх.

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

Якщо ви хочете глибше зануритися в параметризовані запити, я написав про них на Поглиблена безпека Laravel, і я рекомендую перевірити офіційну базу даних і Документація Eloquent.

Перш ніж ми перейдемо далі, давайте швидко подивимося, що міг зробити хакер із цим уразливим запитом.

Щоб додати контекст: коли цей запит виконується, він збільшує кількість ходів у грі для кожного користувача на величину event_increment. Це однаково вигідно всім користувачам у грі.

Якби я був хакером, який виявив цю вразливість, я б хотів збільшити лише свої власні ходи в грі. Я міг би використати низку припущень і методом проб і помилок, щоб створити вхідні дані, які виглядають так, щоб надіслати їх у поле гра:

42 && user_id = (SELECT id FROM users WHERE email = '[email protected]' LIMIT 1)

Подання цього до програми призведе до створення такого SQL-запиту:

DB::statement("UPDATE game_user SET turns += event_increment WHERE game_id = 42 && user_id = (SELECT id FROM users WHERE email = `[email protected]` LIMIT 1)");

Трохи очищено:

UPDATE game_user
SET turns += event_increment
WHERE game_id = 42
  AND user_id = (
    SELECT id
    FROM users
    WHERE email = '[email protected]'
    LIMIT 1
  )

І все.Мої черги збільшаться, але нікого іншого. Дуже просто, все через відсутність перевірки та параметризації запитів.

Це тривіальний приклад, але ви можете зробити БАГАТО за допомогою SQL Injection. Існує багато прийомів для вилучення інформації, зокрема коли сторінка не повертає жодного видимого зворотного зв’язку (або повідомлень про помилки!).Якщо ви хочете дізнатися більше, ми занурилися в ін’єкції SQL у жовтні в Laravel Security in Depth, включаючи навмисно вразливу веб-програму, де ви можете здійснювати власні атаки SQLi. Перегляньте це тут.

Екранування вихідних даних

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

Але є одна річ, яку я хочу підкреслити, перш ніж ми закінчимо, а це правильне екранування даних. Якщо ви пам’ятаєте одну річ, пам’ятайте, що вам слід постаратися, щоб уникати використання неекранованих тегів леза.

Ці речі тут:

{!! $variable !!}

Будь ласка, просто не використовуйте їх.

Ви повинні пам’ятати про те, що ви виводите на сторінку.Розглянемо наші попередні дані, указавши ім’я та прізвище користувача.

Що робити, якщо користувач указав це як своє ім’я:

Stephen <script src="https://evilhacker.dev/evil.js"></script>

А потім ми зробили це на сторінці:

{{ $user->first_name }}

Як і слід було очікувати, ми побачимо їх назву та тег сценарію, надруковані простим текстом. Його було б абсолютно безпечно завантажувати, і ми відразу б дізналися, що вони намагалися ввести якийсь злий JavaScript.

А якби наш код виглядав так:

// Controller
$links = $pages
    ->map(fn ($page) => "<a href=\"{$page->url}\">{$user->first_name}</a>")
    ->join(', ', ' and ');
 
return view('pages', ['links' => $links])
// Template
<div>
    {!! $links !!}
</div>

Все, що ми побачимо, це "Стівена", і шкідливий Javascript буде працювати у браузері, роблячи все, що хоче хакер. Ви можете легко зрозуміти, чому розробник звернувся до цього рішення, але воно залишає програму відкритою для XSS.

Як нам це зробити?

  1. We have the e($value) function, which does the actual escaping of output when you use the {{ $value }} tags. You can call it anywhere.
  2. If you wrap something inside the Illuminate\Support\HtmlString class, it will bypass escaping.
// Controller
$links = $pages
    ->map(fn ($page) => "<a href=\"{$page->url}\">".e($user->first_name)."</a>")
    ->join(', ', ' and ');
 
return view('pages', ['links' => new HtmlString($links)])
// Template
<div>
    {{ $links }}
</div>

Comments

No comments yet
Yurij Finiv

Yurij Finiv

Full stack

Про мене

Professional Fullstack Developer with extensive experience in website and desktop application development. Proficient in a wide range of tools and technologies, including Bootstrap, Tailwind, HTML5, CSS3, PUG, JavaScript, Alpine.js, jQuery, PHP, MODX, and Node.js. Skilled in website development using Symfony, MODX, and Laravel. Experience: Contributed to the development and translation of MODX3 i...

Про автора CrazyBoy49z
WORK EXPERIENCE
Контакти
Ukraine, Lutsk
+380979856297