SQL injection в обход mysql_real_escape_string(): мифы и реальность

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

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

Начать курс

Введение: стоит ли бояться mysql_real_escape_string()?

Вы наверняка слышали старую поговорку PHP-разработчиков: «mysql_real_escape_string() спасает от всех SQL-инъекций». Так ли это на самом деле? В 2024 году, когда PDO и prepared statements стали стандартом, эта тема кажется архаичной. Но тысячи разработчиков до сих пор наследуют старые проекты, где mysql_real_escape_string() — единственная линия обороны.

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

Как работает mysql_real_escape_string()

Функция mysql_real_escape_string() (из расширения mysql_*, ныне deprecated) экранирует специальные символы в строке, учитывая текущую кодировку соединения. Она добавляет обратный слеш перед символами: ', ", \, \n, \r, NULL и Control-Z.

<?php
$connection = mysql_connect('localhost', 'user', 'pass');
mysql_select_db('test', $connection);
$unsafe = "O'Brien";
$safe = mysql_real_escape_string($unsafe); // O\'Brien
$query = "SELECT * FROM users WHERE name = '$safe'";
?>

Кажется, что это работает. Но есть нюансы.

Сценарий 1: Проблема с кодировкой (GBK / Big5)

Самый известный способ обхода — использование многобайтовых кодировок, таких как GBK (китайская) или Big5. Если соединение установлено в GBK, а вы передаёте строку %bf%27 — происходит магия.

В GBK байт 0xbf не является завершённым символом. Когда mysql_real_escape_string() встречает ' (0x27), он добавляет обратный слеш 0x5c. Получается последовательность: 0xbf + 0x5c + 0x27. Но в GBK 0xbf5c — это один валидный символ (например, иероглиф «縗»). В результате слеш «съедается», а кавычка остаётся незащищённой!

<?php
// Установка кодировки GBK
mysql_query("SET NAMES 'GBK'", $connection);
$input = "%bf%27"; // Байты: 0xbf 0x27
$escaped = mysql_real_escape_string($input, $connection);
// Результат: 0xbf5c27 -> в GBK это "縗'" (кавычка осталась)
$query = "SELECT * FROM users WHERE id = '$escaped'";
// Фактически выполняется: SELECT * FROM users WHERE id = '縗''
// Теперь можно инжектить: ' OR 1=1 -- 
?>

Вывод: Если кодировка соединения не совпадает с кодировкой данных, mysql_real_escape_string() может быть обойдена. Именно этот баг был описан в известном баг-репорте PHP #47168.

Сценарий 2: Использование числовых полей

Экранирование работает только со строками. Если вы подставляете данные в числовое поле без кавычек, mysql_real_escape_string() не поможет, потому что значение не экранируется — оно просто вставляется как есть.

<?php
$id = mysql_real_escape_string($_GET['id']); // Допустим, "1 OR 1=1"
$query = "SELECT * FROM users WHERE id = $id"; // Без кавычек!
// Опасность: SELECT * FROM users WHERE id = 1 OR 1=1
?>

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

Вывод: Экранирование не заменяет проверку типа данных. Для чисел используйте intval() или ctype_digit().

Сценарий 3: LIKE-запросы и wildcard-символы

mysql_real_escape_string() экранирует ' и \, но не трогает символы % и _. В операторе LIKE они имеют специальное значение:

  • % — любая последовательность символов
  • _ — ровно один любой символ
<?php
$search = mysql_real_escape_string($_GET['q']); // "admin%"
$query = "SELECT * FROM users WHERE username LIKE '$search'";
// Найдёт всех пользователей, чьё имя начинается с "admin"
// Это не инъекция в классическом смысле, но утечка данных
?>

Хотя это не прямая SQL-инъекция, злоумышленник может использовать % для перебора данных. Например, a% покажет всех, у кого имя начинается на 'a'.

Вывод: Для LIKE-запросов экранируйте % и _ вручную: str_replace(['%', '_'], ['\%', '\_'], $input).

Сравнение подходов: mysql_real_escape_string vs PDO vs MySQLi

Давайте сравним три основных подхода к защите от SQL-инъекций в PHP. Мы рассмотрим их с точки зрения безопасности, удобства и производительности.

1. mysql_real_escape_string() (старый подход)

  • Плюсы: Работает в старых проектах, прост для понимания.
  • Минусы: Требует ручного экранирования каждой переменной; не защищает от ошибок кодировки; легко забыть экранировать; deprecated с PHP 7.0.
  • Безопасность: Средняя. Уязвим при неправильной кодировке и в числовых полях.

2. MySQLi (MySQL Improved)

  • Плюсы: Поддерживает prepared statements; объектно-ориентированный интерфейс; поддерживает транзакции.
  • Минусы: Синтаксис громоздкий; всё ещё привязан к MySQL.
  • Безопасность: Высокая (при использовании prepared statements).
<?php
$mysqli = new mysqli('localhost', 'user', 'pass', 'test');
$stmt = $mysqli->prepare("SELECT * FROM users WHERE id = ?");
$stmt->bind_param("i", $_GET['id']);
$stmt->execute();
$                

Блоги

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