Риски безопасности

Базы данных часто содержат конфиденциальные данные и позволяют выполнять опасные операции. 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 следует использовать только для доверенных значений, определенных в коде приложения. Для значений, предоставленных пользователем, снова используйте белый список.

версия: 4.0