Avatar ·

SQL-инъекция в обход mysql_real_escape_string() — как защититься?

Всем привет! Я начинающий PHP-разработчик, и меня мучает один вопрос. Я читал, что функция `mysql_real_escape_string()` (или её аналог `mysqli_real_escape_string()`) считается надёжной защитой от SQL-инъекций. Но недавно наткнулся на старый пост на Stack Overflow, где утверждается, что её можно обойти. В своём проекте я использую устаревшее расширение `mysql_*` (знаю, что оно deprecated, но проект старый, и пока нет возможности переписать всё на PDO). Вот пример моего кода: ```php $username = mysql_real_escape_string($_POST['username']); $password = mysql_real_escape_string($_POST['password']); $query = "SELECT * FROM users WHERE username='$username' AND password='$password'"; $result = mysql_query($query); ``` Я наивно полагал, что экранирования символов вроде кавычек достаточно. Но в том обсуждении говорилось про кодировки, многобайтовые символы и какую-то магию с `GBK`. Я не совсем понял суть уязвимости. Как именно можно обойти `mysql_real_escape_string()`? И главное — как правильно защитить запросы, если я пока не могу отказаться от старого `mysql_*` расширения? Нужны конкретные примеры атаки и способы защиты. Заранее спасибо за разъяснения! Очень хочется разобраться в этой теме, чтобы не допустить дыр в безопасности. <<>>

Проблема: обход экранирования через многобайтовые кодировки

Уязвимость, описанная в том вопросе, связана с неправильной настройкой кодировки соединения при использовании mysql_real_escape_string() (или её аналога mysqli_real_escape_string()). Функция экранирует спецсимволы, но она зависит от кодировки, установленной для соединения с БД. Если кодировка соединения не совпадает с реальной кодировкой данных, возникает возможность атаки.

Классический пример — кодировка GBK (китайская). В GBK некоторые двухбайтовые символы содержат обратную косую черту \ (0x5c) в качестве второго байта. Если хакер передаёт строку, которая заканчивается на 0xbf, а затем апостроф ' (0x27), то после экранирования получится 0xbf 0x5c 0x27. Первые два байта 0xbf5c интерпретируются как один символ GBK (например, «縗»), а оставшийся 0x27 остаётся как незащищённая одинарная кавычка, что ломает запрос.

Как это выглядит на практике

Допустим, атакующий отправляет в поле username значение:

0xbf27  (в ASCII это символы: ¿')

Ваш код:

$username = mysql_real_escape_string("¿'");  // Кодировка соединения — GBK
// Результат: 0xbf5c27  (¿\' ) — но 0xbf5c — один символ GBK!
// SQL-запрос: SELECT * FROM users WHERE username='縗'' AND password='...'
// Кавычка ' после 縗 остаётся незащищённой!

Теперь атакующий может дописать свой SQL-код после этой кавычки, например:

username = 0xbf27 OR 1=1 -- 
// В результате запрос: SELECT * FROM users WHERE username='縗' OR 1=1 -- '
// Все пользователи будут возвращены!

Решение: правильная настройка кодировки и использование параметризованных запросов

Вариант 1 (для mysqli):

// Устанавливаем кодировку соединения ДО выполнения запросов
$mysqli = new mysqli("localhost", "user", "pass", "db");
$mysqli->set_charset("utf8");  // или "utf8mb4"

// Теперь mysql_real_escape_string() будет работать корректно
$username = $mysqli->real_escape_string($_POST['username']);
$password = $mysqli->real_escape_string($_POST['password']);
$query = "SELECT * FROM users WHERE username='$username' AND password='$password'";

Но это не идеальное решение! Даже с правильной кодировкой остаётся риск ошибок. Лучше использовать подготовленные запросы (prepared statements).

Вариант 2 (рекомендуемый): PDO или mysqli с prepared statements

// PDO (рекомендуется)
$pdo = new PDO("mysql:host=localhost;dbname=test;charset=utf8mb4", "user", "pass");
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);  // Отключаем эмуляцию

$stmt = $pdo->prepare("SELECT * FROM users WHERE username = :username AND password = :password");
$stmt->execute([':username' => $_POST['username'], ':password' => $_POST['password']]);
$result = $stmt->fetchAll();

// Или с mysqli:
$stmt = $mysqli->prepare("SELECT * FROM users WHERE username = ? AND password = ?");
$stmt->bind_param("ss", $_POST['username'], $_POST['password']);
$stmt->execute();
$result = $stmt->get_result()->fetch_all();

При использовании подготовленных запросов данные никогда не вставляются напрямую в SQL-строку. Они передаются отдельно, и БД сама обрабатывает их как данные, а не как код. Это полностью исключает SQL-инъекции, независимо от кодировки.

Что делать, если вы застряли на старом mysql_*?

  1. Убедитесь, что у вас установлена правильная кодировка соединения: mysql_set_charset('utf8').
  2. Как можно скорее перепишите код на PDO или mysqli. Расширение mysql_* удалено из PHP 7.0.
  3. Временно можно использовать двойное экранирование: mysql_real_escape_string() + удаление обратных слешей, но это костыль.

Главный вывод: никогда не полагайтесь только на экранирование. Используйте подготовленные запросы — это единственный надёжный способ.

<<>>

Альтернативный взгляд: фильтрация входных данных

Коллега выше правильно описал проблему с кодировками. Хочу добавить, что дополнительная проверка типов может снизить риски, даже если вы временно используете mysql_real_escape_string().

Для полей, где ожидается число, можно применить приведение к типу:

$id = (int) $_GET['id'];  // Если не число — станет 0
$query = "SELECT * FROM products WHERE id = $id";

Для строковых полей полезно проверять длину и допустимые символы:

$username = $_POST['username'];
if (strlen($username) > 50 || !preg_match('/^[a-zA-Z0-9_]+$/', $username)) {
    die("Некорректное имя пользователя");
}
$username = mysql_real_escape_string($username);

Такой подход не защищает от всех атак (например, если в поле «имя» допускаются любые символы), но сильно сужает поверхность атаки. Комбинируйте его с правильной настройкой кодировки соединения (mysql_set_charset('utf8')) — и вы получите приемлемый уровень безопасности, пока не перейдёте на PDO.

Помните: безопасность — это многослойная защита. Prepared statements — основа, но валидация и санитизация данных на входе — хорошее дополнение.