Приведите пример типичной уязвимости SQL-инъекции в фрагменте PHP/SQL и перепишите код с объяснением, почему безопасный вариант защищён; затем обсудите случаи, когда подготовленные выражения не достаточно (например, динамическая DDL) и как в таких ситуациях минимизировать риск
Пример уязвимого фрагмента и исправление. Уязвимый код (PHP + mysqli), где входной параметр подставляется в строку запроса напрямую: ``` <?php $id = $_GET['id']; // злоумышленник контролирует $sql = "SELECT * FROM users WHERE id = '$id'"; $result = $mysqli->query($sql); ?>
``` Например, при параметре id = `' OR '1'='1` получится условие, которое вернёт все строки (простая SQL‑инъекция). Безопасный вариант (PDO с подготовленным выражением): ``` <?php $pdo = new PDO($dsn, $user, $pass, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]); $id = $_GET['id']; $stmt = $pdo->prepare('SELECT * FROM users WHERE id = :id'); $stmt->execute([':id' => $id]); $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); ?>
``` Почему это безопасно: - Значение параметра передаётся отдельно от текста запроса. СУБД воспринимает `:id` как плейсхолдер и не экранирует его как SQL‑фрагмент. Даже если в `$_GET['id']` будет `\' OR '1'='1`, это будет обработано как строковое значение, а не как часть SQL. - Подготовленные выражения предотвращают класс атак, где пользовательский ввод изменяет структуру запроса. Когда подготовленные выражения недостаточны (примеры и меры по снижению риска) 1) Динамические идентификаторы (имена таблиц/столбцов), фрагменты SQL (ORDER BY, GROUP BY, части DDL) - Проблема: плейсхолдеры обычно работают только для значений, а не для идентификаторов или ключевых слов. Нельзя сделать `SELECT * FROM :table`. - Как минимизировать риск: - Использовать белый список (allow‑list). Принимать от пользователя только заранее разрешённые имена таблиц/столбцов: - пример: если допустимы только таблицы `users` и `articles`, проверять `in_array($table, ['users','articles'])`. - Для числовых параметров явно приводить/валидировать тип: `$limit = (int)$_GET['limit'];` (явное приведение). - Применять корректное экранирование идентификаторов функцией/механизмом СУБД (например, бектик‑кавычки в MySQL) только после проверки белого списка; никогда не экранировать сырые пользовательские строки без проверки. - Составлять динамический SQL из строго контролируемых шаблонов, а не из произвольного ввода. 2) Динамическая DDL (CREATE/ALTER), генерация SQL по пользовательским шаблонам - Проблема: когда приложение формирует DDL на основе входа (имена колонок, типы и т. п.), подготовленные выражения не помогут. - Меры: - Ограничить такие операции только для привилегированных админов и выполнять вручную или через безопасный интерфейс миграций. - Валидировать/нормализовать все идентификаторы через белый список или строгие регулярные выражения (например, разрешать только `[A-Za-z0-9_]+` и дополнительно проверять на совпадение с разрешённым набором). - Логировать и аудировать такие запросы, применять транзакции и бэкапы перед изменением схемы. 3) Вторичные (second‑order) инъекции и логика приложения - Проблема: данные, безопасно сохранённые в БД, могут потом использоваться для построения другого SQL без параметризации. - Меры: - На каждой точке использования данных применять подготовленные выражения и валидацию. - Санитизировать/экрапировать при выводе в другие контексты (HTML, shell и т. п.). Дополнительные защитные меры (рекомендации) - Принцип наименьших привилегий: учётная запись БД для веб‑приложения должна иметь только нужные права (обычно только SELECT/INSERT/UPDATE/DELETE, без DDL). - Использовать ORM или query builder, которые по умолчанию параметризуют значения и помогают с экранированием идентификаторов. - Централизованная валидация и белые списки для всех динамических частей SQL. - Логи и мониторинг подозрительных запросов; WAF как дополнительный уровень (не заменяет корректную защиту в коде). - Ограничивать подробность ошибок, чтобы не выдавать SQL‑ошибки пользователю. Кратко: - Для значений используйте подготовленные выражения/параметризацию — это решает большую часть проблем. - Для динамических идентификаторов и DDL — применяйте белые списки, строгую валидацию, привилегии и отдельные безопасные механизмы управления схемой.
Уязвимый код (PHP + mysqli), где входной параметр подставляется в строку запроса напрямую:
```
<?php
$id = $_GET['id']; // злоумышленник контролирует
$sql = "SELECT * FROM users WHERE id = '$id'";
$result = $mysqli->query($sql);
?> ```
Например, при параметре id = `' OR '1'='1` получится условие, которое вернёт все строки (простая SQL‑инъекция).
Безопасный вариант (PDO с подготовленным выражением):
```
<?php
$pdo = new PDO($dsn, $user, $pass, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
$id = $_GET['id'];
$stmt = $pdo->prepare('SELECT * FROM users WHERE id = :id');
$stmt->execute([':id' => $id]);
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
?> ```
Почему это безопасно:
- Значение параметра передаётся отдельно от текста запроса. СУБД воспринимает `:id` как плейсхолдер и не экранирует его как SQL‑фрагмент. Даже если в `$_GET['id']` будет `\' OR '1'='1`, это будет обработано как строковое значение, а не как часть SQL.
- Подготовленные выражения предотвращают класс атак, где пользовательский ввод изменяет структуру запроса.
Когда подготовленные выражения недостаточны (примеры и меры по снижению риска)
1) Динамические идентификаторы (имена таблиц/столбцов), фрагменты SQL (ORDER BY, GROUP BY, части DDL)
- Проблема: плейсхолдеры обычно работают только для значений, а не для идентификаторов или ключевых слов. Нельзя сделать `SELECT * FROM :table`.
- Как минимизировать риск:
- Использовать белый список (allow‑list). Принимать от пользователя только заранее разрешённые имена таблиц/столбцов:
- пример: если допустимы только таблицы `users` и `articles`, проверять `in_array($table, ['users','articles'])`.
- Для числовых параметров явно приводить/валидировать тип: `$limit = (int)$_GET['limit'];` (явное приведение).
- Применять корректное экранирование идентификаторов функцией/механизмом СУБД (например, бектик‑кавычки в MySQL) только после проверки белого списка; никогда не экранировать сырые пользовательские строки без проверки.
- Составлять динамический SQL из строго контролируемых шаблонов, а не из произвольного ввода.
2) Динамическая DDL (CREATE/ALTER), генерация SQL по пользовательским шаблонам
- Проблема: когда приложение формирует DDL на основе входа (имена колонок, типы и т. п.), подготовленные выражения не помогут.
- Меры:
- Ограничить такие операции только для привилегированных админов и выполнять вручную или через безопасный интерфейс миграций.
- Валидировать/нормализовать все идентификаторы через белый список или строгие регулярные выражения (например, разрешать только `[A-Za-z0-9_]+` и дополнительно проверять на совпадение с разрешённым набором).
- Логировать и аудировать такие запросы, применять транзакции и бэкапы перед изменением схемы.
3) Вторичные (second‑order) инъекции и логика приложения
- Проблема: данные, безопасно сохранённые в БД, могут потом использоваться для построения другого SQL без параметризации.
- Меры:
- На каждой точке использования данных применять подготовленные выражения и валидацию.
- Санитизировать/экрапировать при выводе в другие контексты (HTML, shell и т. п.).
Дополнительные защитные меры (рекомендации)
- Принцип наименьших привилегий: учётная запись БД для веб‑приложения должна иметь только нужные права (обычно только SELECT/INSERT/UPDATE/DELETE, без DDL).
- Использовать ORM или query builder, которые по умолчанию параметризуют значения и помогают с экранированием идентификаторов.
- Централизованная валидация и белые списки для всех динамических частей SQL.
- Логи и мониторинг подозрительных запросов; WAF как дополнительный уровень (не заменяет корректную защиту в коде).
- Ограничивать подробность ошибок, чтобы не выдавать SQL‑ошибки пользователю.
Кратко:
- Для значений используйте подготовленные выражения/параметризацию — это решает большую часть проблем.
- Для динамических идентификаторов и DDL — применяйте белые списки, строгую валидацию, привилегии и отдельные безопасные механизмы управления схемой.