Ризики безпеки
База даних часто містить конфіденційні дані та дозволяє виконувати небезпечні операції. Для безпечної роботи з Nette Database ключовим є:
- Розуміти різницю між безпечним та небезпечним API
- Використовувати параметризовані запити
- Правильно валідувати вхідні дані
Що таке SQL Injection?
SQL injection є найсерйознішим ризиком безпеки при роботі з базою даних. Він виникає, коли необроблені вхідні дані від користувача стають частиною SQL-запиту. Зловмисник може вставити власні SQL-команди і таким чином:
- Отримати несанкціонований доступ до даних
- Змінити або видалити дані в базі даних
- Обійти автентифікацію
// ❌ НЕБЕЗПЕЧНИЙ КОД - вразливий до SQL-ін'єкції
$database->query("SELECT * FROM users WHERE name = '$_GET[name]'");
// Зловмисник може ввести, наприклад, значення: ' OR '1'='1
// Кінцевий запит буде: SELECT * FROM users WHERE name = '' OR '1'='1'
// Що поверне всіх користувачів
Те саме стосується і Database Explorer:
// ❌ НЕБЕЗПЕЧНИЙ КОД - вразливий до SQL-ін'єкції
$table->where('name = ' . $_GET['name']);
$table->where("name = '$_GET[name]'");
Параметризовані запити
Основний захист від SQL injection – це параметризовані запити. Nette Database пропонує кілька способів їх використання.
Найпростіший спосіб – використання заповнювачів-знаків питання:
// ✅ Безпечний параметризований запит
$database->query('SELECT * FROM users WHERE name = ?', $name);
// ✅ Безпечна умова в Explorer
$table->where('name = ?', $name);
Це стосується всіх інших методів у Database Explorer, які дозволяють вставляти вирази з заповнювачами-знаками питання та параметрами.
Для команд INSERT, UPDATE або умови WHERE ми можемо передати значення в масиві:
// ✅ Безпечний INSERT
$database->query('INSERT INTO users', [
'name' => $name,
'email' => $email,
]);
// ✅ Безпечний INSERT в Explorer
$table->insert([
'name' => $name,
'email' => $email,
]);
Валідація значень параметрів
Параметризовані запити є основним будівельним блоком безпечної роботи з базою даних. Однак значення, які ми в них вставляємо, повинні пройти кілька рівнів перевірок:
Перевірка типу
Найважливіше – забезпечити правильний тип даних параметрів – це необхідна умова для безпечного використання Nette Database. База даних передбачає, що всі вхідні дані мають правильний тип даних, що відповідає даному стовпцю.
Наприклад, якби $name
у попередніх прикладах було несподівано
масивом замість рядка, Nette Database спробувала б вставити всі його елементи
в SQL-запит, що призвело б до помилки. Тому ніколи не використовуйте
невалідовані дані з $_GET
, $_POST
або $_COOKIE
безпосередньо в запитах до бази даних.
Перевірка формату
На другому рівні ми перевіряємо формат даних – наприклад, чи є рядки в кодуванні UTF-8 та чи їхня довжина відповідає визначенню стовпця, або чи є числові значення в дозволеному діапазоні для даного типу даних стовпця.
На цьому рівні валідації ми можемо частково покладатися і на саму базу даних – багато баз даних відхилять невалідовані дані. Однак поведінка може відрізнятися, деякі можуть тихо скоротити довгі рядки або обрізати числа поза діапазоном.
Доменна перевірка
Третій рівень представляють логічні перевірки, специфічні для вашого додатка. Наприклад, перевірка, що значення з select box відповідають запропонованим варіантам, що числа знаходяться в очікуваному діапазоні (наприклад, вік 0–150 років) або що взаємні залежності між значеннями мають сенс.
Рекомендовані способи валідації
- Використовуйте Nette Forms, які автоматично забезпечать правильну валідацію всіх вхідних даних
- Використовуйте Presenters та вказуйте у
параметрів в
action*()
таrender*()
методах типи даних - Або реалізуйте власний шар валідації за допомогою стандартних
інструментів PHP, таких як
filter_var()
Безпечна робота зі стовпцями
У попередньому розділі ми показали, як правильно валідувати значення параметрів. Однак при використанні масивів у SQL-запитах ми повинні приділяти таку ж увагу і їхнім ключам.
// ❌ НЕБЕЗПЕЧНИЙ КОД - не оброблені ключі в масиві
$database->query('INSERT INTO users', $_POST);
У командах INSERT та UPDATE це є критичною помилкою безпеки – зловмисник
може вставити або змінити будь-який стовпець у базі даних. Він міг би,
наприклад, встановити is_admin = 1
або вставити будь-які дані в
конфіденційні стовпці (так звана Mass Assignment Vulnerability).
В умовах WHERE це ще небезпечніше, оскільки вони можуть містити оператори:
// ❌ НЕБЕЗПЕЧНИЙ КОД - не оброблені ключі в масиві
$_POST['salary >'] = 100000;
$database->query('SELECT * FROM users WHERE', $_POST);
// виконає запит WHERE (`salary` > 100000)
Зловмисник може використати цей підхід для систематичного з'ясування зарплат співробітників. Наприклад, почне із запиту на зарплати понад 100 000, потім менше 50 000 і поступовим звуженням діапазону може виявити приблизні зарплати всіх співробітників. Цей тип атаки називається SQL enumeration.
Методи where()
та whereOr()
є ще набагато гнучкішими і підтримують у
ключах та значеннях SQL-вирази, включаючи оператори та функції. Це дає
зловмиснику можливість здійснити SQL-ін'єкцію:
// ❌ НЕБЕЗПЕЧНИЙ КОД - зловмисник може вставити власний SQL
$_POST = ['0) UNION SELECT name, salary FROM users WHERE (1'];
$table->where($_POST);
// виконає запит WHERE (0) UNION SELECT name, salary FROM users WHERE (1)
Ця атака завершує початкову умову за допомогою 0)
, приєднує
власний SELECT
за допомогою UNION
для отримання
конфіденційних даних з таблиці users
та закриває синтаксично
правильний запит за допомогою WHERE (1)
.
Білий список стовпців
Для безпечної роботи з назвами стовпців нам потрібен механізм, який забезпечить, що користувач може працювати лише з дозволеними стовпцями і не може додати власні. Ми могли б спробувати виявляти та блокувати небезпечні назви стовпців (чорний список), але цей підхід ненадійний – зловмисник завжди може придумати новий спосіб записати небезпечну назву стовпця, який ми не передбачили.
Тому набагато безпечніше змінити логіку і визначити явний список дозволених стовпців (білий список):
// Стовпці, які користувач може редагувати
$allowedColumns = ['name', 'email', 'active'];
// Видалимо всі недозволені стовпці з вхідних даних
$filteredData = array_intersect_key($userData, array_flip($allowedColumns));
// ✅ Тепер можна безпечно використовувати в запитах, наприклад:
$database->query('INSERT INTO users', $filteredData);
$table->update($filteredData);
$table->where($filteredData);
Динамічні ідентифікатори
Для динамічних назв таблиць та стовпців використовуйте заповнювач
?name
. Він забезпечить правильне екранування ідентифікаторів
відповідно до синтаксису даної бази даних (наприклад, за допомогою
зворотних апострофів у MySQL):
// ✅ Безпечне використання довірених ідентифікаторів
$table = 'users';
$column = 'name';
$database->query('SELECT ?name FROM ?name', $column, $table);
// Результат у MySQL: SELECT `name` FROM `users`
Важливо: символ ?name
використовуйте лише для довірених значень,
визначених у коді програми. Для значень від користувача
використовуйте знову білий список. Інакше ви
наражаєтеся на ризики безпеки:
// ❌ НЕБЕЗПЕЧНО - ніколи не використовуйте вхідні дані від користувача
$database->query('SELECT ?name FROM users', $_GET['column']);