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