Риски безопасности
База данных часто содержит конфиденциальные данные и позволяет выполнять опасные операции. Для безопасной работы с Nette Database ключевыми являются:
- Понимание разницы между безопасным и небезопасным API
- Использование параметризованных запросов
- Правильная валидация входных данных
Что такое SQL Injection?
SQL Injection — это самый серьезный риск безопасности при работе с базой данных. Он возникает, когда необработанные входные данные от пользователя становятся частью 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 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, которые автоматически обеспечивают правильную валидацию всех входных данных
- Используйте Presenters и указывайте типы
данных для параметров в методах
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']);