Введение: стоит ли бояться 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();
$