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