«Спасите! Моя 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 '%%%'
Это вернёт все товары. Не инъекция, но утечка данных. А если комбинировать с другими уязвимостями — можно вытащить всю базу.