Avatar ·

Как предотвратить SQL-инъекции в PHP? (полное руководство)

Всем привет! Я начинающий PHP-разработчик, и у меня возникла серьёзная проблема с безопасностью. Я пишу простую форму входа на сайт, и мой код выглядит примерно так: ```php $username = $_POST['username']; $password = $_POST['password']; $query = "SELECT * FROM users WHERE username='$username' AND password='$password'"; $result = mysqli_query($conn, $query); ``` Я знаю, что это ужасно небезопасно, но не понимаю, как это исправить правильно. Друзья сказали, что меня могут взломать через SQL-инъекцию. Я попробовал ввести в поле логина что-то вроде `admin' OR '1'='1`, и действительно — меня пустило в систему без пароля! Я читал про `mysqli_real_escape_string()`, но слышал, что это не панацея. Ещё говорят про PDO и prepared statements, но я не понимаю, как их правильно использовать. Мой проект работает на PHP 8.1 с MySQL. Пожалуйста, объясните подробно: 1. Как именно работает SQL-инъекция? 2. Какие есть современные способы защиты? 3. Какой метод считается лучшей практикой в 2024 году? 4. Покажите рабочие примеры кода. Заранее спасибо! Очень хочу писать безопасный код. <<>>

Что такое SQL-инъекция и почему это опасно?

SQL-инъекция — это атака, при которой злоумышленник вставляет вредоносный SQL-код в запрос через пользовательский ввод. В вашем примере строка admin' OR '1'='1 превращает запрос в:

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

Условие OR '1'='1' всегда истинно, поэтому запрос возвращает первого пользователя в таблице — и злоумышленник получает доступ.

Лучший способ защиты: Prepared Statements (подготовленные запросы)

Современный стандарт — использовать PDO (PHP Data Objects) с prepared statements. Они отделяют структуру запроса от данных, поэтому ввод пользователя никогда не интерпретируется как код SQL.

Пример правильного кода с PDO

<?php
// Подключение к БД
$host = 'localhost';
$dbname = 'mydb';
$user = 'root';
$pass = '';

try {
    $pdo = new PDO("mysql:host=$host;dbname=$dbname;charset=utf8mb4", $user, $pass);
    $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch (PDOException $e) {
    die("Ошибка подключения: " . $e->getMessage());
}

// Безопасный запрос с prepared statement
$sql = "SELECT * FROM users WHERE username = :username AND password = :password";
$stmt = $pdo->prepare($sql);

// Привязка параметров
$stmt->bindParam(':username', $username);
$stmt->bindParam(':password', $password);

// Получение данных от пользователя
$username = $_POST['username'];
$password = $_POST['password'];

// Выполнение запроса
$stmt->execute();

// Получение результатов
$user = $stmt->fetch(PDO::FETCH_ASSOC);
if ($user) {
    echo "Вход выполнен успешно!";
} else {
    echo "Неверный логин или пароль";
}
?>

Почему это безопасно?

  • Разделение кода и данных: SQL-запрос компилируется до того, как в него подставляются значения. Даже если пользователь введёт ' OR '1'='1, это будет просто строка, а не часть SQL.
  • Автоматическое экранирование: PDO сам экранирует специальные символы.
  • Защита от всех типов инъекций: Подходит для INSERT, UPDATE, DELETE, SELECT с любыми условиями.

Альтернатива: MySQLi с prepared statements

Если вы используете MySQLi, тоже можно применять prepared statements:

$stmt = $conn->prepare("SELECT * FROM users WHERE username = ? AND password = ?");
$stmt->bind_param("ss", $username, $password); // "ss" — две строки
$username = $_POST['username'];
$password = $_POST['password'];
$stmt->execute();
$result = $stmt->get_result();

Чего НЕ стоит делать

  • Не используйте mysqli_real_escape_string() как основную защиту — она не защищает от всех атак (например, при атаках с многобайтовыми кодировками).
  • Не конкатенируйте строки — даже если вы экранируете каждую переменную, человеческий фактор (забыли экранировать одно поле) может привести к уязвимости.
  • Не доверяйте фильтрации на стороне клиента — JavaScript-валидацию можно обойти.

Дополнительные меры безопасности

  • Храните пароли только в хешированном виде (password_hash() / password_verify()).
  • Используйте подготовленные запросы для всех запросов, даже для "безопасных" данных.
  • Включайте строгий режим ошибок PDO (ERRMODE_EXCEPTION) для отлова проблем.
  • Ограничьте права пользователя БД (не используйте root в приложении).

Запомните главное правило: никогда не вставляйте пользовательские данные напрямую в SQL-запрос. Используйте подготовленные запросы — это просто, надёжно и защищает от 99% атак.

<<>>

Дополнительный подход: Использование ORM (Object-Relational Mapping)

Если вы не хотите писать SQL-запросы вручную, рассмотрите использование ORM, таких как Doctrine или Eloquent (из Laravel). Они полностью абстрагируют работу с базой данных и автоматически используют prepared statements под капотом.

Пример с Doctrine DBAL (без фреймворка)

use Doctrine\DBAL\DriverManager;

$connectionParams = [
    'dbname' => 'mydb',
    'user' => 'root',
    'password' => '',
    'host' => 'localhost',
    'driver' => 'pdo_mysql',
];

$conn = DriverManager::getConnection($connectionParams);

$sql = "SELECT * FROM users WHERE username = ? AND password = ?";
$stmt = $conn->executeQuery($sql, [$username, $password]);
$user = $stmt->fetchAssociative();

Пример с Eloquent (Laravel)

$user = User::where('username', $username)
            ->where('password', $password)
            ->first();

Плюсы ORM:

  • Автоматическая защита: Все запросы используют prepared statements.
  • Читаемость кода: Меньше шаблонного кода.
  • Переносимость: Легко сменить СУБД (MySQL → PostgreSQL).
  • Дополнительные функции: Миграции, валидация, отношения между таблицами.

Минусы:

  • Немного медленнее для простых запросов (из-за overhead).
  • Требует изучения API ORM.
  • Для сложных запросов может быть менее гибким, чем чистый SQL.

Если ваш проект растёт, ORM — отличный выбор. Но для небольших скриптов или обучения лучше начать с PDO и prepared statements, чтобы понимать, как работает защита "под капотом".