SQL injection в обход mysql_real_escape_string(): разбор уязвимостей

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

«Спасите! Моя mysql_real_escape_string() не работает!»

Если вы когда-нибудь гуглили «SQL injection that gets around mysql_real_escape_string()», вы не одиноки. Этот вопрос на Stack Overflow собрал десятки тысяч просмотров и стал классикой веб-безопасности. Казалось бы, берёшь mysql_real_escape_string() — и ты защищён. Но нет. Хакеры находят лазейки, а разработчики — седые волосы.

В этой статье мы разберём, как именно SQL-инъекции могут обойти экранирование mysql_real_escape_string() в PHP. Вы увидите реальные примеры кода, поймёте, почему «старый добрый» подход больше не работает, и узнаете, как защититься по-настоящему. Поехали!

Почему mysql_real_escape_string() не панацея?

Для начала — немного истории. Функция mysql_real_escape_string() появилась в эпоху PHP 4 и 5, когда все сидели на расширении mysql_*. Она экранирует специальные символы вроде кавычек, обратных слешей и управляющих символов. Но проблема в том, что она работает только в контексте одной кодировки.

Если кодировка клиента и сервера не совпадают — начинается ад. А если вы используете многобайтовые кодировки (например, GBK или SJIS) — функция может стать вашим злейшим врагом.

Классический пример: мультибайтовая атака

Представьте, что ваш сайт использует кодировку GBK (китайская), а вы передаёте строку:

$_POST['name'] = "0xbf27"; // Экранированный ' или просто '?

В GBK символ 0xbf27 — это валидный китайский иероглиф. Но если mysql_real_escape_string() «увидит» 0x27 (апостроф), она экранирует его обратным слешем: 0xbf5c27. Проблема в том, что 0xbf5c — это тоже валидный символ в GBK! Получается, что апостроф 0x27 остаётся неэкранированным, и хакер может вставить свой SQL-код.

// Уязвимый код
$name = mysql_real_escape_string($_POST['name']);
$query = "SELECT * FROM users WHERE name = '$name'";

// Если ввести: 0xbf27
// Функция вернёт: 0xbf5c27
// В GBK это: китайский символ + апостроф
// Запрос становится: SELECT * FROM users WHERE name = 'кит_символ' OR 1=1 -- '

Вуаля! Инъекция удалась. И это не теория — такие уязвимости находили в реальных проектах.

Типичные уязвимости при использовании mysql_real_escape_string()

Давайте перечислим самые частые ошибки, которые позволяют обойти экранирование:

  • Неправильная кодировка — как в примере выше. Если сервер думает, что данные в UTF-8, а клиент шлёт GBK, экранирование ломается.
  • Использование числовых полей без кавычек — если вы подставляете число напрямую, экранирование не сработает. Например: SELECT * FROM users WHERE id = $id — и хакер пишет 1 OR 1=1.
  • LIKE-запросы — экранирование не обрабатывает символы % и _. Можно провести Blind SQL injection через шаблоны.
  • Многократное экранирование — если вы вызываете функцию дважды, обратный слеш удваивается, и защита пропадает.

Пример: атака на числовое поле

// Уязвимый код
$id = mysql_real_escape_string($_GET['id']);
$query = "SELECT * FROM products WHERE id = $id"; // Кавычки не нужны!

// Хакер вводит: 1 UNION SELECT username, password FROM users
// Запрос становится: SELECT * FROM products WHERE id = 1 UNION SELECT username, password FROM users

Экранирование здесь бесполезно — нет кавычек, которые нужно экранировать. Хакер просто вставляет свой SQL напрямую.

Современные методы защиты (которые работают)

Хватит страшилок — давайте к решениям. Если вы пишете на PHP и хотите забыть о SQL-инъекциях, вот ваш чек-лист:

1. Используйте PDO или MySQLi с prepared statements

Это единственный надёжный способ. Prepared statements отделяют данные от кода: SQL-шаблон отправляется на сервер, а значения передаются отдельно. Экранирование вообще не нужно — база данных сама обрабатывает данные.

// Безопасный код с PDO
$stmt = $pdo->prepare("SELECT * FROM users WHERE name = :name");
$stmt->execute([':name' => $_POST['name']]);
$users = $stmt->fetchAll();

// Безопасный код с MySQLi
$stmt = $mysqli->prepare("SELECT * FROM users WHERE name = ?");
$stmt->bind_param("s", $_POST['name']);
$stmt->execute();
$result = $stmt->get_result();

Обратите внимание: никакой mysql_real_escape_string() не нужен. Prepared statements решают проблему раз и навсегда.

2. Задавайте кодировку явно

Если вы по каким-то причинам всё ещё используете старый mysql_* (не надо!), обязательно укажите кодировку:

mysql_set_charset('utf8', $connection);

Но лучше — переходите на PDO и задавайте кодировку через DSN:

$pdo = new PDO('mysql:host=localhost;dbname=test;charset=utf8', 'user', 'pass');

3. Валидируйте типы данных

Для числовых полей всегда проверяйте, что пришло число:

$id = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT);
if ($id === false) {
    die('Некорректный ID');
}

Это простое правило закрывает целый класс атак.

Реальный кейс: как хакеры обходят экранирование в LIKE

Представьте поиск по товарам:

$search = mysql_real_escape_string($_GET['q']);
$query = "SELECT * FROM products WHERE name LIKE '%$search%'";

Хакер может ввести %' OR 1=1 -- — и экранирование сработает, превратив кавычку в \'. Но что, если он введёт просто %? Запрос станет:

SELECT * FROM products WHERE name LIKE '%%%'

Это вернёт все товары. Не инъекция, но утечка данных. А если комбинировать с другими уязвимостями — можно вытащить всю базу.

Почему mysql_real_

Блоги

Рекомендаций по книгам