Как предотвратить SQL-инъекции в PHP? (полное руководство)
Что такое 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, чтобы понимать, как работает защита "под капотом".