SQL-инъекция в PHP 2025: как защититься? Полное руководство

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение: почему SQL-инъекция всё ещё актуальна в 2025?

Представьте: вы написали крутой сайт на PHP, добавили форму обратной связи, поиск по товарам, а через неделю обнаруживаете, что злоумышленник выгрузил всю базу данных клиентов. Звучит как страшилка из 2010-х? Увы, по данным OWASP, SQL-инъекция (SQL injection) до сих пор входит в топ-3 самых опасных уязвимостей веб-приложений. И да, в 2025 году разработчики всё ещё допускают эту ошибку.

Вопрос "How can I prevent SQL injection in PHP?" на Stack Overflow собрал тысячи просмотров и сотни ответов. Но проблема не в отсутствии информации — проблема в том, что многие до сих пор используют устаревшие методы вроде mysql_* функций (которые удалили ещё в PHP 7.0) или наивно полагаются на mysqli_real_escape_string() как на серебряную пулю.

В этой статье мы разберём лучшие практики защиты от SQL-инъекций в PHP в 2025 году. Вы узнаете, почему подготовленные запросы (prepared statements) — это стандарт индустрии, как правильно работать с PDO и mysqli, и какие грабли всё ещё ждут неопытных разработчиков. Поехали!

Что такое SQL-инъекция простыми словами?

SQL-инъекция — это атака, при которой злоумышленник "впрыскивает" вредоносный SQL-код в запрос через пользовательский ввод (формы, URL, куки). Вместо того чтобы просто передать имя пользователя, хакер может отправить: ' OR '1'='1 — и получить доступ ко всем данным.

Вот классический пример уязвимого кода:

// НИКОГДА так не делайте!
$username = $_POST['username'];
$password = $_POST['password'];
$query = "SELECT * FROM users WHERE username = '$username' AND password = '$password'";
$result = mysqli_query($conn, $query);

Если злоумышленник введёт в поле username: admin' --, то запрос превратится в:

SELECT * FROM users WHERE username = 'admin' --' AND password = '...'

Всё, что после -- — комментарий. Аутентификация пройдена. База данных — наша. Привет, утечка данных!

Метод №1 (и самый главный): Подготовленные запросы (Prepared Statements)

В 2025 году это единственный рекомендуемый способ защиты от SQL-инъекций. Подготовленные запросы отделяют логику SQL от данных. Вы сначала говорите базе данных: "Вот шаблон запроса, вот места для переменных", а потом передаёте данные отдельно. База данных сама экранирует всё, что нужно.

Вариант A: PDO (PHP Data Objects) — современный стандарт

PDO поддерживает 12 различных СУБД (MySQL, PostgreSQL, SQLite и другие). Если вы пишете новый проект в 2025 году — используйте PDO.

<?php
// Подключение к БД через PDO
$dsn = 'mysql:host=localhost;dbname=testdb;charset=utf8mb4';
$user = 'root';
$password = '';

try {
    $pdo = new PDO($dsn, $user, $password);
    // Важно: выбрасывать исключения при ошибках
    $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch (PDOException $e) {
    die('Ошибка подключения: ' . $e->getMessage());
}

// Подготовленный запрос с именованными плейсхолдерами
$stmt = $pdo->prepare('SELECT * FROM users WHERE email = :email AND status = :status');
$stmt->execute([
    ':email' => $_POST['email'],
    ':status' => 'active'
]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);

// Альтернатива с позиционными плейсхолдерами (?)
$stmt = $pdo->prepare('SELECT * FROM users WHERE email = ? AND status = ?');
$stmt->execute([$_POST['email'], 'active']);
$user = $stmt->fetch();
?>

Почему это безопасно? Потому что данные никогда не вставляются напрямую в SQL-строку. Даже если пользователь передаст ' OR '1'='1, PDO передаст это как строковый литерал, и база данных воспримет его именно как строку, а не как часть SQL-команды.

Вариант B: MySQLi (MySQL Improved) — если проект legacy

Если вы работаете со старым проектом на MySQLi — тоже можно использовать подготовленные запросы:

<?php
$mysqli = new mysqli('localhost', 'root', '', 'testdb');

// Проверка соединения
if ($mysqli->connect_error) {
    die('Connect Error: ' . $mysqli->connect_error);
}

// Подготовленный запрос
$stmt = $mysqli->prepare('SELECT * FROM users WHERE email = ? AND status = ?');
$stmt->bind_param('ss', $email, $status); // s = string, i = integer, d = double, b = blob

$email = $_POST['email'];
$status = 'active';
$stmt->execute();

$result = $stmt->get_result();
$user = $result->fetch_assoc();

$stmt->close();
$mysqli->close();
?>

Обратите внимание: мы передаём тип данных в bind_param() — это дополнительный уровень защиты. Если вы ожидаете число, укажите 'i'.

Метод №2: Экранирование строк (и почему это не панацея)

Функция mysqli_real_escape_string() экранирует специальные символы в строке. Но это не защищает от всех видов инъекций. Например, она бесполезна, если вы вставляете данные в числовое поле без кавычек:

// Уязвимо! ID не в кавычках
$id = mysqli_real_escape_string($conn, $_GET['id']);
$query = "SELECT * FROM products WHERE id = $id";

Злоумышленник может передать 1 UNION SELECT ... — и экранирование не сработает. Поэтому:

  • Используйте mysqli_real_escape_string() только если вы не можете использовать подготовленные запросы (например, для динамических LIKE-запросов).
  • Всегда приводите числовые поля к integer: $id = (int)$_GET['id'];
  • Для LIKE используйте экранирование + кастомная обработка символов % и _.

Метод №3: Белые списки (Whitelisting) для динамических запросов

Иногда нужно динамически формировать ORDER BY, LIMIT или имя таблицы. Подготовленные запросы не работают для этих частей SQL. Решение — белый список разрешённых значений:

<?php
// Динамическая сортировка — только разрешённые поля
$allowedColumns = ['name', 'price', 'created_at'];
$sort = $_GET['sort']                

Блоги

Рекомендаций по книгам