Avatar ·

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

Всем привет! Я начинающий PHP-разработчик и столкнулся с очень странной ситуацией. На многих форумах пишут, что функция `mysql_real_escape_string()` — это надёжная защита от SQL-инъекций, и я всегда ей пользовался. Но недавно прочитал, что есть способы её обойти, и это меня очень напугало. У меня есть простой код для авторизации пользователя: ```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 или SJIS), то можно подобрать такую последовательность символов, которая "съест" экранирующий слеш и позволит вставить произвольный SQL-код. Это правда? Как именно это работает? И главное — как мне теперь правильно защищать свои запросы? Неужели нужно отказываться от `mysql_real_escape_string()` и переходить на что-то другое? Буду благодарен за подробное объяснение с примерами кода! <<>>

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

Да, вы правы. Функция mysql_real_escape_string() (и её устаревший собрат mysql_escape_string()) действительно имеет уязвимость при работе с некоторыми многобайтовыми кодировками, такими как GBK, BIG5, SJIS. Проблема возникает из-за того, что экранирующий символ обратного слеша (\) может быть "поглощён" предыдущим байтом, если тот образует с ним валидный многобайтовый символ.

Как это работает на практике

Рассмотрим кодировку GBK. В ней байты 0xbf и 0x27 образуют один символ. Если злоумышленник передаёт в поле username строку:

0xbf 0x27  OR 1=1 -- 

То после экранирования функцией mysql_real_escape_string() кавычка 0x27 превратится в 0x5c 0x27 (слеш + кавычка). Но если кодировка соединения установлена как GBK, MySQL воспринимает последовательность 0xbf 0x5c как один символ (китайский иероглиф), а оставшаяся кавычка 0x27 становится самостоятельной и закрывает строку. В итоге запрос выглядит так:

SELECT * FROM users WHERE username='縗' OR 1=1 -- ' AND password='...'

Видите? Кавычка, которую мы хотели экранировать, на самом деле осталась "голой" и завершила строку. Всё, что идёт после неё, интерпретируется как SQL-код. Инъекция удалась!

Почему это происходит?

  • mysql_real_escape_string() вставляет слеш \ (байт 0x5c) перед опасными символами.
  • Если кодировка соединения — многобайтовая, MySQL может интерпретировать пару байтов 0xbf 0x5c как один символ.
  • Следующий байт 0x27 (кавычка) уже не экранируется и становится управляющим.

Как защититься? Три надёжных способа

1. Переход на подготовленные выражения (PDO или MySQLi)

Это самый правильный и современный подход. Подготовленные выражения отделяют структуру запроса от данных, поэтому экранирование вообще не требуется. Пример на PDO:

$pdo = new PDO('mysql:host=localhost;dbname=test;charset=utf8', 'user', 'pass');
$stmt = $pdo->prepare("SELECT * FROM users WHERE username = :username AND password = :password");
$stmt->execute([
    ':username' => $_POST['username'],
    ':password' => $_POST['password']
]);
$user = $stmt->fetch();

Здесь данные никогда не встраиваются в SQL-строку — они передаются отдельно. Даже если в них есть кавычки или слеши, они не могут повлиять на структуру запроса.

2. Использование MySQLi с подготовленными выражениями

Если вы не хотите переходить на PDO, используйте MySQLi:

$mysqli = new mysqli('localhost', 'user', 'pass', 'test');
$stmt = $mysqli->prepare("SELECT * FROM users WHERE username = ? AND password = ?");
$stmt->bind_param('ss', $_POST['username'], $_POST['password']);
$stmt->execute();
$result = $stmt->get_result();
$user = $result->fetch_assoc();

3. Если нужно остаться на старом mysql_* — установите правильную кодировку

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

$conn = mysql_connect('localhost', 'user', 'pass');
mysql_select_db('test', $conn);
mysql_set_charset('utf8', $conn); // или 'latin1', но не GBK/SJIS

$username = mysql_real_escape_string($_POST['username']);
$query = "SELECT * FROM users WHERE username='$username'";

Обратите внимание: mysql_set_charset() сообщает MySQL, в какой кодировке приходят данные, и экранирование будет корректным. Никогда не используйте SET NAMES или SET CHARACTER SET — это не работает должным образом с устаревшим расширением.

Итог

Лучшая защита — вообще не использовать mysql_real_escape_string(). Переходите на PDO или MySQLi с подготовленными выражениями. Они не только безопасны, но и делают код чище и читаемее. А старые функции mysql_* официально удалены из PHP 7, так что их использование — это ещё и проблема совместимости.

<<>>

Альтернативный подход: белые списки и фильтрация

Подготовленные выражения — это идеал, но иногда (например, при построении динамических ORDER BY или LIMIT) они не работают, так как эти части SQL не могут быть параметризованы. В таких случаях используйте строгую фильтрацию через белые списки.

Пример для сортировки:

$allowedSort = ['id', 'name', 'date'];
$sort = $_GET['sort'] ?? 'id';
if (!in_array($sort, $allowedSort)) {
    $sort = 'id';
}

$query = "SELECT * FROM users ORDER BY $sort";

Для числовых данных (LIMIT, OFFSET) принудительно приводите к целому числу:

$limit = (int)$_GET['limit']; // если не число, будет 0
$query = "SELECT * FROM users LIMIT $limit";

Дополнительный совет: используйте современные ORM

Если вы работаете с фреймворками (Laravel, Symfony), они уже используют PDO и подготовленные выражения под капотом. Например, в Laravel Eloquent:

$user = User::where('username', $request->username)
            ->where('password', Hash::make($request->password))
            ->first();

Это не только безопасно, но и удобно. Никакого ручного экранирования — всё автоматически защищено.

Помните: mysql_real_escape_string() — это костыль из прошлого. Современный PHP предлагает гораздо более надёжные инструменты. Используйте их!