MySQLSeniorTechnical

Как предотвратить SQL injection при работе с MySQL?

SQL-инъекции предотвращаются параметризованными запросами (prepared statements) — никогда не интерполируйте пользовательский ввод в SQL. Дополнительно: принцип минимальных привилегий и WAF.

Предотвращение SQL-инъекций в MySQL

SQL-инъекция — атака, при которой вредоносный ввод изменяет структуру SQL-запроса. Единственная надёжная защита — параметризованные запросы, при которых данные и код SQL разделены на уровне протокола.

Уязвимый код (никогда не делайте так)

// УЯЗВИМО: прямая конкатенация
$query = "SELECT * FROM users WHERE username = '" . $_GET['user'] . "'";
// Ввод: admin' OR '1'='1
// Итоговый запрос: SELECT * FROM users WHERE username = 'admin' OR '1'='1'

Защита 1: Prepared Statements (PDO)

$pdo = new PDO('mysql:host=127.0.0.1;dbname=app', $user, $pass, [
    PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_EMULATE_PREPARES   => false, // критично! реальные prepared statements
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);

$stmt = $pdo->prepare(
    'SELECT id, email FROM users WHERE username = :username AND active = :active'
);
$stmt->execute([
    ':username' => $username,  // данные никогда не попадают в SQL-текст
    ':active'   => 1,
]);
$user = $stmt->fetch();

Защита 2: MySQLi с bind_param

$mysqli = new mysqli('127.0.0.1', $user, $pass, 'app');
$mysqli->set_charset('utf8mb4'); // обязательно!

$stmt = $mysqli->prepare('SELECT id FROM users WHERE email = ? AND role = ?');
$stmt->bind_param('ss', $email, $role); // 's' = string, 'i' = int, 'd' = double
$stmt->execute();
$result = $stmt->get_result();

Защита 3: ORM (Eloquent / Doctrine)

// Eloquent автоматически параметризует
$users = User::where('email', $email)
             ->where('role', $role)
             ->get();

// Raw-запросы в Eloquent тоже безопасны через биндинг
$users = DB::select(
    'SELECT * FROM users WHERE email = ? AND created_at > ?',
    [$email, $date]
);

// ОПАСНО: DB::statement с интерполяцией:
// DB::statement("DELETE FROM users WHERE id = $id"); // никогда!

Принцип минимальных привилегий

-- Создаём отдельного пользователя для приложения
CREATE USER 'app_user'@'%' IDENTIFIED BY 'strong_password';
-- Только необходимые права — никакого SUPER, DROP, FILE
GRANT SELECT, INSERT, UPDATE, DELETE ON appdb.* TO 'app_user'@'%';
-- Для readonly-реплики:
GRANT SELECT ON appdb.* TO 'app_readonly'@'%';
FLUSH PRIVILEGES;

Дополнительные меры

  • Валидация и whitelist: если нужно динамически подставлять имя таблицы или колонки — валидируйте через whitelist, биндинг не поддерживает идентификаторы.
  • Хранимые процедуры: инкапсулируют SQL и принимают параметры — не панацея, но уменьшают поверхность атаки.
  • WAF: ModSecurity с OWASP CRS или AWS WAF с правилами для SQL-инъекций как дополнительный слой.
  • Аудит: MySQL Audit Log Plugin или Percona Audit Log — фиксируют все запросы к БД для forensics.

Подводные камни

  • PDO::ATTR_EMULATE_PREPARES = true (значение по умолчанию!) делает биндинг иллюзорным — PDO сам интерполирует данные в строку; отключайте явно.
  • Биндинг не защищает от инъекций в идентификаторы (ORDER BY $column) — используйте whitelist разрешённых значений.
  • htmlspecialchars() и addslashes() не защищают от SQL-инъекций — это инструменты для XSS и не заменяют параметризацию.
  • Second-order injection: данные сохраняются «безопасно», но потом используются в другом запросе без параметризации — часто упускают при code review.
  • Кодировка: без set_charset('utf8mb4') в MySQLi многобайтные символы могут обойти экранирование в старых версиях.
  • ORM-методы типа whereRaw(), selectRaw(), orderByRaw() принимают сырой SQL — вставка переменных без биндинга так же уязвима.
  • Сообщения об ошибках БД в ответе приложения раскрывают структуру таблиц — всегда логируйте ошибки сервер-сайд, пользователю возвращайте generic message.
  • Недостаточно параметризовать только WHERE — LIKE-паттерны с % и _ внутри биндинга остаются мета-символами; экранируйте их дополнительно.

Common mistakes

  • Не уточнять storage engine и говорить о предотвращение sql injection так, будто MyISAM и InnoDB ведут себя одинаково.
  • Игнорировать implicit commit, autocommit, isolation level или binlog там, где они меняют ответ.
  • Описывать PostgreSQL-поведение как MySQL-поведение.
  • Забывать проверить план через EXPLAIN и фактическую версию MySQL.

What the interviewer is testing

  • Кандидат объясняет предотвращение sql injection через реальный механизм MySQL, а не общими словами.
  • Приводит корректный SQL или диагностический запрос для этой СУБД.
  • Называет ограничения, версионные отличия или эксплуатационные последствия.
  • Связывает ответ с проектированием приложения, производительностью, надежностью или безопасностью.

Sources

Related topics