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

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

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

// ❌ ОПАСНЫЙ КОД - уязвимость к 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);

Это относится ко всем остальным методам в 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 или вставить произвольные данные в чувствительные столбцы (известная как уязвимость массового назначения).

В условиях WHERE это еще опаснее, поскольку они могут содержать операторы:

// ❌ ОПАСНЫЙ КОД - ключи массива не подвергаются санитарной обработке
$_POST['salary >'] = 100000;
$database->query('SELECT * FROM users WHERE', $_POST);
// выполняет запрос WHERE (`salary` > 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`

Важно: Используйте символ ?name только для доверенных значений, определенных в коде приложения. Для значений, предоставленных пользователем, снова используйте белый список. В противном случае вы рискуете получить уязвимости в системе безопасности:

// ❌ ОПАСНО - никогда не используйте пользовательский ввод
$database->query('SELECT ?name FROM users', $_GET['column']);
версия: 4.0