«Хороший сайт делается для людей, а поисковики приходят следом»

— из принципов редакции SeoLiga.ru
Главная » Статьи » Веб-программирование » Работа с MySQL из PHP: безопасные запросы

Работа с MySQL из PHP: безопасные запросы

Почти любой динамический сайт хранит данные в базе: пользователей, заказы, статьи, комментарии. А значит, где-то в коде есть строки, формирующие SQL-запросы. И если они собраны небрежно, злоумышленник может изменить смысл запроса, получить чужие данные или вовсе удалить таблицы. Эта проблема называется SQL-инъекцией, и она до сих пор встречается даже в довольно серьёзных проектах. Разберём, как работать с MySQL из PHP безопасно и без лишних мучений.

Как возникает SQL-инъекция

Представьте типичный код, который ищет пользователя по логину из формы:

$login = $_POST['login'];
$sql = "SELECT * FROM users WHERE login = '$login'";
$result = $db->query($sql);

Пока человек вводит обычное имя, всё работает. Но если вместо логина ввести строку с кавычкой и дополнительным условием, например ' OR '1'='1, итоговый запрос изменит смысл и вернёт всех пользователей. А с более хитрыми конструкциями можно обойти авторизацию, прочитать другие таблицы или изменить данные. Корень проблемы: пользовательский ввод напрямую склеивается с текстом запроса, и база не может отличить команду от данных.

Главное решение: подготовленные запросы

Правильный подход — разделить текст запроса и значения. Сначала база получает «шаблон» с метками вместо данных, затем отдельно — сами значения. Они подставляются как данные и никогда не интерпретируются как часть команды. Это основной и самый надёжный способ защиты.

В PHP для этого есть два основных расширения: PDO и mysqli. Оба поддерживают подготовленные запросы. Старое расширение с функциями с префиксом mysql_ давно устарело и удалено — если вы встречаете его в коде, его нужно заменить.

Подключение через PDO

PDO — универсальный слой доступа к данным, удобный и лаконичный. Типичное подключение выглядит так:

$dsn = 'mysql:host=localhost;dbname=mysite;charset=utf8mb4';

$options = [
    PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
    PDO::ATTR_EMULATE_PREPARES   => false,
];

try {
    $pdo = new PDO($dsn, $user, $password, $options);
} catch (PDOException $e) {
    error_log($e->getMessage());
    exit('Сервис временно недоступен');
}

Разберём важные настройки:

  • charset=utf8mb4 — корректная кодировка, включая все символы и эмодзи; без неё возможны искажения данных и ошибки.
  • ERRMODE_EXCEPTION — ошибки превращаются в исключения, их удобно ловить и не пропускать.
  • FETCH_ASSOC — результат возвращается в виде ассоциативного массива без дублирования.
  • EMULATE_PREPARES выключен — подготовка выполняется на стороне сервера базы, что надёжнее.

Обратите внимание на обработку ошибки подключения: подробности пишем в журнал, а пользователю показываем нейтральное сообщение. Выводить на экран текст ошибки с адресом сервера и именем пользователя нельзя.

Выборка, вставка, обновление

Безопасный вариант поиска пользователя:

$stmt = $pdo->prepare('SELECT id, login, email FROM users WHERE login = :login');
$stmt->execute(['login' => $login]);
$user = $stmt->fetch();

Вставка и обновление работают по тому же принципу:

$stmt = $pdo->prepare('INSERT INTO comments (post_id, author, text) VALUES (?, ?, ?)');
$stmt->execute([$postId, $author, $text]);
$newId = $pdo->lastInsertId();

$stmt = $pdo->prepare('UPDATE users SET email = :email WHERE id = :id');
$stmt->execute(['email' => $email, 'id' => $id]);
echo $stmt->rowCount();

Можно использовать именованные метки (:login) или позиционные (?). Именованные читаются легче, особенно в длинных запросах. Не смешивайте оба стиля в одном запросе.

Что нельзя подставить через метки

Подготовленные запросы защищают значения, но не умеют подставлять имена таблиц, столбцов и направление сортировки. Часто разработчики в этом месте возвращаются к склейке строк и снова открывают дыру. Решение — белый список допустимых значений:

$allowed = ['id', 'created', 'title'];
$sort = in_array($_GET['sort'] ?? '', $allowed, true) ? $_GET['sort'] : 'id';

$dir = (($_GET['dir'] ?? '') === 'desc') ? 'DESC' : 'ASC';

$stmt = $pdo->query("SELECT id, title FROM posts ORDER BY $sort $dir");

Здесь пользовательское значение не попадает в запрос напрямую: оно сравнивается с заранее известным списком, и в SQL уходит только проверенный вариант.

То же касается оператора IN со списком значений. Нужно сгенерировать нужное число меток и передать массив:

$ids = [3, 7, 12];
$marks = implode(',', array_fill(0, count($ids), '?'));
$stmt = $pdo->prepare("SELECT * FROM posts WHERE id IN ($marks)");
$stmt->execute($ids);

Для конструкции LIKE символы подстановки добавляйте к значению, а не к запросу: передайте строку вида '%' . $word . '%' как параметр. При необходимости экранируйте в ней служебные символы процента и подчёркивания.

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

Подготовленные запросы — главный рубеж, но не единственный. Хорошая защита многослойна:

  • Минимальные права. Пользователь базы, под которым работает сайт, должен иметь только необходимые привилегии на нужные таблицы. Если сайту не нужно удалять таблицы или менять структуру, этих прав у него быть не должно.
  • Отдельные учётные записи. Для разных проектов и ролей (чтение, запись, администрирование) заводите разные аккаунты.
  • Валидация ввода. Проверяйте тип, длину и формат данных: идентификатор — число, адрес почты — корректный адрес. Приводите числа к целому типу.
  • Хранение паролей. Используйте встроенные функции хеширования паролей, а не простые хеши и тем более открытый текст.
  • Экранирование при выводе. Защита от внедрения скриптов — отдельная задача: данные, выводимые в HTML, нужно обрабатывать функцией экранирования.
  • Скрытие ошибок. На рабочем сервере подробные сообщения базы не должны показываться посетителям.
  • Конфиденциальность настроек. Пароли к базе храните вне публичной папки сайта или в переменных окружения.

Транзакции

Когда несколько операций должны выполниться вместе или не выполниться вовсе (например, списание со счёта и запись в журнал), используйте транзакции:

try {
    $pdo->beginTransaction();

    $pdo->prepare('UPDATE accounts SET balance = balance - ? WHERE id = ?')
        ->execute([$sum, $from]);
    $pdo->prepare('UPDATE accounts SET balance = balance + ? WHERE id = ?')
        ->execute([$sum, $to]);

    $pdo->commit();
} catch (Exception $e) {
    $pdo->rollBack();
    error_log($e->getMessage());
}

Для таблиц должен использоваться движок, поддерживающий транзакции. Это защищает от «половинчатых» состояний данных при сбоях.

Типичные ошибки

  • Использовать функции экранирования строк вместо подготовленных запросов — они легко ошибочно применяются и не защищают от всех сценариев.
  • Доверять данным из скрытых полей, куки и заголовков: всё, что пришло от клиента, может быть подделано.
  • Подставлять в запрос идентификаторы из адресной строки без проверки.
  • Показывать пользователю текст ошибки базы.
  • Запускать сайт под учётной записью администратора базы.
  • Копировать старые примеры из интернета с устаревшими функциями.

Как проверить свой код

Найдите в проекте все места, где формируются запросы, и просмотрите их глазами. Подозрительны любые конструкции, где в строку запроса через точку или в кавычках попадают переменные. Затем попробуйте ввести в формы кавычки и служебные символы: страницы не должны ломаться и показывать ошибки базы. Для более серьёзной проверки используйте автоматические сканеры уязвимостей и статический анализ кода — они находят типовые проблемы быстрее ручного поиска.

Заключение

Безопасная работа с MySQL в PHP держится на простом правиле: данные и команды никогда не смешиваются. Используйте PDO или mysqli с подготовленными запросами, белые списки для имён и сортировки, минимальные права базы и аккуратную обработку ошибок. Эти привычки почти ничего не стоят при написании нового кода, но избавляют от самых неприятных последствий. Возьмите на себя полчаса и пройдитесь по своим проектам: скорее всего, пара уязвимых мест найдётся именно там, где вы давно не заглядывали.

ПО
Павел Орлов
Автор SeoLiga.ru. Пишет о практике: что работает на реальных проектах, а что лучше обойти стороной. Хотите предложить свою статью? Загляните в раздел «Авторам».