SQL-инъекция в обход mysql_real_escape_string() — как защититься?
Проблема: обход экранирования через многобайтовые кодировки
Уязвимость, описанная в том вопросе, связана с неправильной настройкой кодировки соединения при использовании mysql_real_escape_string() (или её аналога mysqli_real_escape_string()). Функция экранирует спецсимволы, но она зависит от кодировки, установленной для соединения с БД. Если кодировка соединения не совпадает с реальной кодировкой данных, возникает возможность атаки.
Классический пример — кодировка GBK (китайская). В GBK некоторые двухбайтовые символы содержат обратную косую черту \ (0x5c) в качестве второго байта. Если хакер передаёт строку, которая заканчивается на 0xbf, а затем апостроф ' (0x27), то после экранирования получится 0xbf 0x5c 0x27. Первые два байта 0xbf5c интерпретируются как один символ GBK (например, «縗»), а оставшийся 0x27 остаётся как незащищённая одинарная кавычка, что ломает запрос.
Как это выглядит на практике
Допустим, атакующий отправляет в поле username значение:
0xbf27 (в ASCII это символы: ¿')
Ваш код:
$username = mysql_real_escape_string("¿'"); // Кодировка соединения — GBK
// Результат: 0xbf5c27 (¿\' ) — но 0xbf5c — один символ GBK!
// SQL-запрос: SELECT * FROM users WHERE username='縗'' AND password='...'
// Кавычка ' после 縗 остаётся незащищённой!
Теперь атакующий может дописать свой SQL-код после этой кавычки, например:
username = 0xbf27 OR 1=1 --
// В результате запрос: SELECT * FROM users WHERE username='縗' OR 1=1 -- '
// Все пользователи будут возвращены!
Решение: правильная настройка кодировки и использование параметризованных запросов
Вариант 1 (для mysqli):
// Устанавливаем кодировку соединения ДО выполнения запросов
$mysqli = new mysqli("localhost", "user", "pass", "db");
$mysqli->set_charset("utf8"); // или "utf8mb4"
// Теперь mysql_real_escape_string() будет работать корректно
$username = $mysqli->real_escape_string($_POST['username']);
$password = $mysqli->real_escape_string($_POST['password']);
$query = "SELECT * FROM users WHERE username='$username' AND password='$password'";
Но это не идеальное решение! Даже с правильной кодировкой остаётся риск ошибок. Лучше использовать подготовленные запросы (prepared statements).
Вариант 2 (рекомендуемый): PDO или mysqli с prepared statements
// PDO (рекомендуется)
$pdo = new PDO("mysql:host=localhost;dbname=test;charset=utf8mb4", "user", "pass");
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); // Отключаем эмуляцию
$stmt = $pdo->prepare("SELECT * FROM users WHERE username = :username AND password = :password");
$stmt->execute([':username' => $_POST['username'], ':password' => $_POST['password']]);
$result = $stmt->fetchAll();
// Или с mysqli:
$stmt = $mysqli->prepare("SELECT * FROM users WHERE username = ? AND password = ?");
$stmt->bind_param("ss", $_POST['username'], $_POST['password']);
$stmt->execute();
$result = $stmt->get_result()->fetch_all();
При использовании подготовленных запросов данные никогда не вставляются напрямую в SQL-строку. Они передаются отдельно, и БД сама обрабатывает их как данные, а не как код. Это полностью исключает SQL-инъекции, независимо от кодировки.
Что делать, если вы застряли на старом mysql_*?
- Убедитесь, что у вас установлена правильная кодировка соединения:
mysql_set_charset('utf8'). - Как можно скорее перепишите код на PDO или mysqli. Расширение mysql_* удалено из PHP 7.0.
- Временно можно использовать двойное экранирование:
mysql_real_escape_string()+ удаление обратных слешей, но это костыль.
Главный вывод: никогда не полагайтесь только на экранирование. Используйте подготовленные запросы — это единственный надёжный способ.
<<Альтернативный взгляд: фильтрация входных данных
Коллега выше правильно описал проблему с кодировками. Хочу добавить, что дополнительная проверка типов может снизить риски, даже если вы временно используете mysql_real_escape_string().
Для полей, где ожидается число, можно применить приведение к типу:
$id = (int) $_GET['id']; // Если не число — станет 0
$query = "SELECT * FROM products WHERE id = $id";
Для строковых полей полезно проверять длину и допустимые символы:
$username = $_POST['username'];
if (strlen($username) > 50 || !preg_match('/^[a-zA-Z0-9_]+$/', $username)) {
die("Некорректное имя пользователя");
}
$username = mysql_real_escape_string($username);
Такой подход не защищает от всех атак (например, если в поле «имя» допускаются любые символы), но сильно сужает поверхность атаки. Комбинируйте его с правильной настройкой кодировки соединения (mysql_set_charset('utf8')) — и вы получите приемлемый уровень безопасности, пока не перейдёте на PDO.
Помните: безопасность — это многослойная защита. Prepared statements — основа, но валидация и санитизация данных на входе — хорошее дополнение.
Ответов пока нет.
Войдите чтобы оставить ответ
Блоги