Ризики безпеки
Бази даних часто містять конфіденційні дані і дозволяють виконувати небезпечні операції. 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'
// Це поверне всіх користувачів!
Те ж саме стосується і Провідника баз даних:
// ❌ КОД НЕБЕЗПЕКИ
$table->where('name = ' . $_GET['name']);
$table->where("name = '$_GET[name]'");
Безпечні параметризовані запити
Безпечний спосіб вставляти значення в SQL-запити – це параметризовані запити. Nette Database надає кілька способів їх використання.
Знаки питання-заповнювачі
Найпростіший метод – використання знаків питання-заповнювачів:
// ✅ Безпечні параметризовані запити
$database->query('SELECT * FROM users WHERE name = ?', $_GET['name']);
// ✅ Безпечний стан в Провіднику
$table->where('name = ?', $_GET['name']);
Те саме стосується всіх інших методів у Провіднику баз даних, які дозволяють вставляти вирази зі знаками питання-заповнювачами та параметрами.
Значення повинні бути скалярного типу (string
, int
,
float
, bool
) або null
. Якщо, наприклад, $_GET['name']
є
масивом, Nette Database включить всі його елементи в SQL-запит, що може бути
небажаним.
Масиви значень
Для речень INSERT
, UPDATE
або WHERE
ми можемо
використовувати масиви значень:
// ✅ Безпечна ВСТАВКА
$database->query('INSERT INTO users', [
'name' => $_GET['name'],
'email' => $_GET['email'],
]);
// Безпечне Оновлення
$database->query('UPDATE users SET', [
'name' => $_GET['name'],
'email' => $_GET['email'],
], 'WHERE id = ?', $_GET['id']);
База даних Nette автоматично екранує всі значення, передані через параметризовані запити. Однак, ми повинні переконатися, що параметри мають правильний тип даних.
Масивні ключі не є безпечним API
Хоча значення в масивах є безпечними, цього не можна сказати про ключі:
// ❌ НЕБЕЗПЕЧНИЙ КОД - ключі можуть містити SQL-ін'єкції
$database->query('INSERT INTO users', $_GET);
$database->query('SELECT * FROM users WHERE', $_GET);
$table->where($_GET);
Для команд INSERT
і UPDATE
це критичний недолік безпеки –
зловмисник може вставити або змінити будь-який стовпець в базі даних.
Наприклад, він може встановити is_admin = 1
або вставити довільні
дані в конфіденційні стовпці.
В умовах WHERE
це ще більш небезпечно, оскільки дозволяє
SQL-перебір – техніку поступового отримання інформації про базу
даних. Зловмисник може спробувати дослідити зарплати співробітників,
вставивши дані в $_GET
таким чином:
$_GET = ['salary >', 100000]; // починає визначати діапазони заробітних плат
Основна проблема, однак, полягає в тому, що умови WHERE
підтримують SQL-вирази в ключах:
// Легальне використання операторів у ключах
$table->where([
'age > ?' => 18,
'ROUND(score, ?) > ?' => [2, 75.5],
]);
// НЕБЕЗПЕЧНО: зловмисник може вставити свій власний SQL
$_GET = ['1) UNION SELECT name, salary FROM users WHERE (is_admin = ?' => 1];
$table->where($_GET); // дозволяє зловмиснику отримувати зарплату адміністратора
Це знову ж таки SQL ін'єкція.
Стовпці білого списку
Якщо ви хочете дозволити користувачам вибирати колонки, завжди використовуйте білий список:
// ✅ Безпечна обробка - тільки дозволені стовпці
$allowedColumns = ['name', 'email', 'active'];
$values = array_intersect_key($_GET, array_flip($allowedColumns));
$database->query('INSERT INTO users', $values);
Динамічні ідентифікатори
Для динамічних назв таблиць і стовпців використовуйте заповнювач
?name
:
// ✅ Безпечне використання довірених ідентифікаторів
$table = 'users';
$column = 'name';
$database->query('SELECT ?name FROM ?name', $column, $table);
// НЕБЕЗПЕЧНО - ніколи не використовуйте введення користувача
$database->query('SELECT ?name FROM users', $_GET['column']);
Символ ?name
слід використовувати лише для довірених значень,
визначених у коді програми. Для значень, наданих користувачем, знову
використовуйте білий список.