[Go to site: main page, start]

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

Базы данных часто содержат конфиденциальные данные и позволяют выполнять опасные операции. Для безопасной работы с Nette Database принципиально важно:

  • Понимать разницу между безопасным и небезопасным API
  • Использовать параметризованные запросы
  • Правильно проверять входные данные

Что такое SQL injection?

SQL injection – самый серьёзный риск безопасности при работе с базами данных. Она возникает, когда неочищенный пользовательский ввод становится частью SQL-запроса. Злоумышленник может вставить собственные SQL-команды и тем самым:

  • получить несанкционированный доступ к данным
  • изменить или удалить данные в базе
  • обойти аутентификацию
// ❌ ОПАСНЫЙ КОД - уязвим для SQL injection
$database->query("SELECT * FROM users WHERE name = '$_GET[name]'");

// Злоумышленник может ввести значение вроде: ' OR '1'='1
// Получившийся запрос будет: SELECT * FROM users WHERE name = '' OR '1'='1'
// Он вернёт всех пользователей

То же относится и к Database Explorer:

// ❌ ОПАСНЫЙ КОД - уязвим для SQL injection
$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 и их длина соответствует описанию столбца или что числовые значения находятся в допустимом диапазоне для типа данных столбца.

На этом уровне проверки мы можем частично положиться на саму базу данных: многие базы отклонят некорректные данные. Однако поведение может различаться: некоторые молча обрежут длинные строки или числа вне диапазона.

Проверка, свойственная предметной области

Третий уровень – логические проверки, специфичные для вашего приложения. Например, проверка, что значения из выпадающих списков соответствуют предложенным вариантам, что числа находятся в ожидаемом диапазоне (например, возраст 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 (`salary` > 100000)

Злоумышленник может таким способом планомерно выяснить зарплаты сотрудников. Он может начать, например, с запроса о зарплатах выше 100 000, затем ниже 50 000 и, постепенно сужая диапазон, раскрыть примерные зарплаты всех сотрудников. Такой вид атаки называется SQL enumeration.

Методы where() и whereOr() ещё гораздо гибче и поддерживают SQL-выражения, включая операторы и функции, в ключах и значениях. Это даёт злоумышленнику возможность выполнить SQL injection:

// ❌ ОПАСНЫЙ КОД - злоумышленник может вставить собственный 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']);
версия: 4.x