SQL-инъекции в PHP: как предотвратить атаки на базу данных
Понимание SQL-инъекций и методы защиты в PHP
SQL-инъекция - это атака, при которой злоумышленник внедряет вредоносный SQL-код в запрос, выполняемый приложением. В PHP такие уязвимости возникают, когда пользовательские данные напрямую конкатенируются в строку запроса. Рассмотрим основные подходы к защите.
Использование подготовленных выражений (PDO)
Как гарантированно защитить запрос от инъекций при любом типе данных?
Наиболее надёжный способ - применение подготовленных выражений через PDO или MySQLi. Запрос и данные передаются раздельно, драйвер автоматически экранирует значения.
$pdo = new PDO('mysql:host=localhost;dbname=test;charset=utf8', 'user', 'pass');
$stmt = $pdo->prepare('SELECT * FROM users WHERE email = :email');
$stmt->execute(['email' => $_POST['email']]);
$user = $stmt->fetch();Php class sql (класс для работы с sql в php)
Объяснение:
- Создаётся соединение с БД через PDO.
- Метод prepare разбирает запрос без подстановки данных.
- Плейсхолдер :email заменяется значением при выполнении.
- Данные обрабатываются как строки, что исключает интерпретацию SQL-команд.
Типичная ошибка: использование PDO::query() с конкатенацией. Если запрос динамически собирается со вставкой переменных, защита теряется.
Проблема: при именованных плейсхолдерах количество плейсхолдеров должно строго соответствовать переданным значениям.
Как защититься с помощью экранирования специальных символов?
Функция mysqli_real_escape_string() экранирует опасные символы, но применима только для строковых значений и не защищает от инъекций в числовых полях, если данные не приведены к типу.
$mysqli = new mysqli('localhost', 'user', 'pass', 'test');
$safe_name = $mysqli->real_escape_string($_POST['name']);
$result = $mysqli->query("SELECT * FROM users WHERE name = '$safe_name'");Php sql insert (insert в php)
Цель: подходит для устаревших проектов, где нельзя перейти на PDO. Применяется с осторожностью - необходимо оборачивать значение в кавычки.
Ошибка: забыть обернуть экранированную строку в кавычки - запрос станет синтаксически неверным. Нельзя использовать для чисел напрямую (потребуется принудительное приведение типа).
Как фильтровать данные с помощью приведения типов для числовых полей?
Если ожидается целое число, можно привести значение к целому перед вставкой в запрос.
$id = (int)$_GET['id'];
$result = $mysqli->query("SELECT * FROM articles WHERE id = $id");Php ms sql (работа с ms sql в php)
Используется, когда поле заведомо числовое и нет необходимости в строковых значениях. Подходит для идентификаторов.
Проблема: не работает для строк, дат или других типов. Придётся комбинировать с другими методами.
Как защитить динамический ORDER BY с помощью белого списка?
Для сортировки нельзя использовать подготовленные выражения - имя столбца не может быть плейсхолдером. Решение - проверка по белому списку.
$allowed = ['id', 'title', 'date'];
$order = in_array($_GET['order'], $allowed) ? $_GET['order'] : 'id';
$result = $mysqli->query("SELECT * FROM posts ORDER BY $order");переменную sql php (использование переменных в sql-запросах php)
Цель: применяется для параметров, которые изменяют структуру запроса (ORDER BY, LIMIT, названия таблиц).
Ошибка: если белый список не содержит все возможные значения, пользователь может получить неверные данные. Нужно всегда задавать значение по умолчанию.
Как использовать ORM для автоматической защиты от инъекций?
ORM (Object-Relational Mapping) библиотеки, такие как Eloquent или Doctrine, сами используют подготовленные выражения.
$user = User::where('email', $_POST['email'])->first();
Подходит для современных фреймворков (Laravel, Symfony). Упрощает код и минимизирует риск человеческой ошибки.
Проблема: ORM добавляет накладные расходы и может быть избыточен для простых скриптов.
Расширенные примеры SQL-инъекций и защиты
Демонстрация уязвимости и разных техник защиты с полным кодом и результатами.
Пример 1. Инъекция через неэкранированный параметр
Незащищённый код:
$conn = new mysqli('localhost', 'root', '', 'test');
$email = $_GET['email'];
$sql = "SELECT * FROM users WHERE email = '$email'";
$result = $conn->query($sql);
При передаче email=test@mail.com' OR '1'='1 запрос становится:
SELECT * FROM users WHERE email = 'test@mail.com' OR '1'='1'
Результат: все строки таблицы. Данные скомпрометированы.
Пример 2. Защита через подготовленные выражения (PDO)
$pdo = new PDO('mysql:host=localhost;dbname=test;charset=utf8', 'root', '');
$stmt = $pdo->prepare('SELECT * FROM users WHERE email = :email');
$stmt->execute([':email' => $_GET['email']]);
$users = $stmt->fetchAll();
Даже если email содержит ' OR '1'='1, запрос остаётся параметризованным - значение целиком сравнивается как строка. Инъекция невозможна.
Пример 3. Использование LIKE с параметрами
Для поиска с LIKE необходимо экранировать символы подстановки или передавать их в параметре:
$search = '%' . $pdo->quote($_POST['search']) . '%'; // неверно
// Правильно: использовать подготовленное выражение
$stmt = $pdo->prepare('SELECT * FROM products WHERE name LIKE :search');
$stmt->execute([':search' => '%' . $_POST['search'] . '%']);
$results = $stmt->fetchAll();
Пояснение: PDO сам экранирует данные внутри плейсхолдера, символ процента воспринимается буквально, если не является частью строки. Для включения подстановки - добавляем проценты до выполнения.
Пример 4. Динамический IN() с массивом значений
Создание списка плейсхолдеров для каждого элемента массива:
$ids = [1, 2, 3];
$placeholders = implode(',', array_fill(0, count($ids), '?'));
$stmt = $pdo->prepare("SELECT * FROM orders WHERE id IN ($placeholders)");
$stmt->execute($ids);
$orders = $stmt->fetchAll();
Результат: выполняется запрос с тремя параметрами - безопасно даже при нечисловых значениях (PDO приводит их к типу).
Пример 5. Защита ORDER BY через белый список
Безопасная сортировка:
$allowed = ['price', 'name', 'created_at'];
$sort = $_GET['sort'];
if (!in_array($sort, $allowed)) {
$sort = 'created_at';
}
$stmt = $pdo->prepare("SELECT * FROM products ORDER BY $sort");
$stmt->execute();
$products = $stmt->fetchAll();
Если злоумышленник передаст sort=price DESC; SELECT * FROM admin, запрос не выполнится - имя столбца не пройдёт проверку белого списка.
Пример 6. Использование MySQLi с bind_param
Аналог PDO на MySQLi:
$stmt = $mysqli->prepare('SELECT * FROM users WHERE email = ?');
$stmt->bind_param('s', $_POST['email']);
$stmt->execute();
$result = $stmt->get_result();
$user = $result->fetch_assoc();
Пояснение: символ ? - позиционный плейсхолдер, bind_param указывает тип ('s' - строка). Данные не интерпретируются как SQL.