Ризики безпеки
Бази даних часто містять конфіденційні дані та дозволяють виконувати небезпечні операції. Для безпечної роботи з базами даних Nette Database ключовими аспектами є
- Розуміння різниці між безпечним і небезпечним API
- Використання параметризованих запитів
- Правильна перевірка вхідних даних
Що таке SQL-ін'єкція?
SQL-ін'єкція – це найсерйозніший ризик безпеки при роботі з базами даних. Вона виникає, коли нефільтрований користувацький ввід стає частиною SQL-запиту. Зловмисник може вставити свої власні SQL-команди і таким чином:
- Витягти несанкціоновані дані
- Змінити або видалити дані в базі даних
- обійти автентифікацію
// ❌ НЕБЕЗПЕЧНИЙ КОД - вразливий до SQL ін'єкції
$database->query("SELECT * FROM users WHERE name = '$_GET[name]'");
// Зловмисник може ввести значення на кшталт ' OR '1'='1
// Результуючий запит буде виглядати наступним чином: SELECT * FROM users WHERE name = '' OR '1'='1'
// Який повертає всіх користувачів
Те ж саме стосується і Провідника баз даних:
// ❌ НЕБЕЗПЕЧНИЙ КОД - вразливий до SQL-ін'єкцій
$table->where('name = ' . $_GET['name']);
$table->where("name = '$_GET[name]'");
Безпечні параметризовані запити
Фундаментальним захистом від SQL-ін'єкцій є параметризовані запити. Nette Database надає кілька способів їх використання.
Найпростіший спосіб – це використання заповнювачів знаків питання:
// ✅ Безпечний параметризований запит
$database->query('SELECT * FROM users WHERE name = ?', $name);
// ✅ Безпечний стан в Провіднику
$table->where('name = ?', $name);
Це стосується всіх інших методів у Провіднику бази даних, які дозволяють вставляти вирази із заповнювачами знаків питання та параметрами.
Для речень INSERT
, UPDATE
або WHERE
ви можете передавати
значення у вигляді масиву:
// ✅ Безпечна вставка
$database->query('INSERT INTO users', [
'name' => $name,
'email' => $email,
]);
// Безпечна ВСТАВКА в Провіднику
$table->insert([
'name' => $name,
'email' => $email,
]);
Перевірка значень параметрів
Параметризовані запити є наріжним каменем безпечної роботи з базами даних. Однак значення, що передаються в них, повинні проходити кілька рівнів перевірки:
Перевірка типу
Забезпечення правильного типу даних параметрів є критично важливим – це необхідна умова для безпечного використання бази даних Nette. База даних припускає, що всі вхідні дані мають правильний тип даних, що відповідає стовпцю.
Наприклад, якщо $name
у попередніх прикладах несподівано став
масивом, а не рядком, Nette Database спробує вставити всі його елементи в
SQL-запит, що призведе до помилки. Тому ніколи не використовуйте
неперевірені дані з $_GET
, $_POST
або $_COOKIE
безпосередньо в запитах до бази даних.
Перевірка формату
На другому рівні перевіряється формат даних – наприклад, кодування рядків у UTF-8 і відповідність їхньої довжини визначенню стовпця, або перевірка того, що числові значення знаходяться в межах допустимого діапазону для типу даних стовпця.
На цьому рівні ви можете частково покладатися на саму базу даних – багато баз даних відкидають невірні дані. Однак поведінка може бути різною: деякі з них можуть мовчки обрізати довгі рядки або вирізати числа, що виходять за межі діапазону.
Перевірка для конкретного домену
Третій рівень включає логічні перевірки, специфічні для вашої програми. Наприклад, перевірка відповідності значень у вибраних полях доступним опціям, відповідності чисел очікуваному діапазону (наприклад, вік від 0 до 150 років) або логічності взаємозв'язків між значеннями.
Рекомендовані методи валідації
- Використовуйте Nette Forms, які автоматично обробляють належну валідацію всіх вхідних даних.
- Використовуйте презентації та
оголошуйте типи даних параметрів у методах
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 (`заробітна плата` > 100000)
Зловмисник може використовувати цей підхід для систематичного виявлення зарплат співробітників. Він може почати із запиту зарплат вище 100 000, потім нижче 50 000, і, поступово звужуючи діапазон, він може виявити приблизні зарплати всіх співробітників. Цей тип атаки називається SQL-перерахуванням.
Методи 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` (SELECT `name` FROM `users`)
Важливо: Використовуйте символ ?name
лише для довірених значень,
визначених у коді програми. Для значень, наданих користувачем, знову
використовуйте білий список. В іншому випадку ви
ризикуєте отримати вразливості в системі безпеки:
// НЕБЕЗПЕЧНО - ніколи не використовуйте введення користувача
$database->query('SELECT ?name FROM users', $_GET['column']);