Краткий ответ: НЕТ , подготовка к работе с PDO не защитит вас от всех возможных атак SQL-инъекций. Для некоторых неясных крайностей.
Я адаптирую этот ответ , чтобы говорить о PDO ...
Длинный ответ не так прост. Он основан на атаке, показанной здесь .
Атака
Итак, начнем с показа атаки ...
$pdo->query('SET NAMES gbk');
$var = "\xbf\x27 OR 1=1 /*";
$query = 'SELECT * FROM test WHERE name = ? LIMIT 1';
$stmt = $pdo->prepare($query);
$stmt->execute(array($var));
При определенных обстоятельствах это вернет более 1 строки. Давайте рассмотрим, что здесь происходит:
Выбор набора символов
$pdo->query('SET NAMES gbk');
Чтобы эта атака работала, нам нужна кодировка, которую сервер ожидает на соединении для кодирования '
, как в ASCII, то есть 0x27
и , чтобы иметь некоторый символ, последний байт которого является ASCII \
т.е. 0x5c
. Как оказалось, в MySQL 5.6 по умолчанию поддерживается 5 таких кодировок: big5
, cp932
, gb2312
, gbk
и sjis
. Мы выберем gbk
здесь.
Теперь очень важно отметить использование SET NAMES
здесь. Это устанавливает набор символов ON SERVER . Есть еще один способ сделать это, но мы скоро туда доберемся.
Полезная нагрузка
Полезная нагрузка, которую мы собираемся использовать для этой инъекции, начинается с последовательности байтов 0xbf27
. В gbk
это недопустимый многобайтовый символ; в latin1
это строка ¿'
. Обратите внимание, что в latin1
и gbk
, 0x27
сам по себе является буквальным '
символом.
Мы выбрали эту полезную нагрузку, потому что, если бы мы вызвали addslashes()
на ней, мы вставили бы ASCII \
, т.е. 0x5c
, перед символом '
. Таким образом, мы получим 0xbf5c27
, который в gbk
представляет собой последовательность из двух символов: 0xbf5c
, за которой следует 0x27
. Или, другими словами, действительный символ, за которым следует неоткрытый '
. Но мы не используем addslashes()
. Итак, перейдем к следующему шагу ...
$ stmt-> Execute ()
Здесь важно понять, что PDO по умолчанию NOT делает действительно подготовленные операторы. Он имитирует их (для MySQL). Поэтому PDO внутренне строит строку запроса, вызывая mysql_real_escape_string()
(функция MySQL C API) для каждого значения связанной строки.
Вызов API C на mysql_real_escape_string()
отличается от addslashes()
тем, что он знает набор символов соединения. Таким образом, он может выполнить экранирование правильно для набора символов, который ожидает сервер. Однако до этого момента клиент думал, что мы все еще используем latin1
для соединения, потому что мы никогда не говорили об этом иначе. Мы сказали серверу , что используем gbk
, но клиент по-прежнему считает, что latin1
.
Следовательно, вызов mysql_real_escape_string()
вставляет обратную косую черту, и у нас есть свободно висящий символ '
в нашем «экранированном» контенте! Фактически, если бы мы смотрели на $var
в наборе символов gbk
, мы бы увидели:
縗' OR 1=1 /*
Именно этого требует атака.
Запрос
Эта часть просто формальность, но вот обработанный запрос:
SELECT * FROM test WHERE name = '縗' OR 1=1 /*' LIMIT 1
Поздравляем, вы только что успешно атаковали программу с использованием подготовленных операторов PDO ...
Простое исправление
Теперь стоит отметить, что вы можете предотвратить это, отключив эмулированные подготовленные операторы:
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
Это , обычно , приводит к истинно подготовленному утверждению (т. Е. Данные отправляются в отдельном пакете из запроса). Тем не менее, имейте в виду, что PDO будет отступать для эмуляции операторов, которые MySQL не может подготовить изначально: те, которые могут быть перечислены в руководстве, но будьте осторожны, чтобы выбрать соответствующую версию сервера ).
Правильное исправление
Проблема здесь в том, что мы не вызывали API-интерфейс C mysql_set_charset()
вместо SET NAMES
. Если бы мы это сделали, нам было бы хорошо, если бы мы использовали релиз MySQL с 2006 года.
Если вы используете более раннюю версию MySQL, то ошибка в mysql_real_escape_string()
означала, что недопустимые многобайтовые символы, такие как наши полезные данные, обрабатывались как одиночные байты для экранирования , даже если клиент был правильно проинформирован о кодировке соединения , и поэтому эта атака все равно будет успешной. Ошибка была исправлена в MySQL 4.1.20 , 5.0.22 и 5.1.11 .
Но хуже всего то, что PDO
не выставлял API C для mysql_set_charset()
до 5.3.6, поэтому в предыдущих версиях не мог предотвратить эту атаку для каждой возможной команды!
Теперь он отображается как параметр DSN , который следует использовать вместо SET NAMES
...
Благодать
Как мы уже говорили, чтобы атака работала, соединение с базой данных должно быть закодировано с использованием уязвимого набора символов. utf8mb4
является не уязвимым и все же может поддерживать каждый символ Unicode: так что вы можете использовать его вместо & mdash; но он был доступен только с MySQL 5.5 0,3. Альтернатива - utf8
, которая также не уязвима и может поддерживать весь Unicode Базовая многоязычная плоскость .
В качестве альтернативы, вы можете включить режим SQL NO_BACKSLASH_ESCAPES
, который (среди прочего) изменяет работу mysql_real_escape_string()
. Если этот режим включен, 0x27
будет заменен на 0x2727
, а не 0x5c27
, и, таким образом, экранирующий процесс не может создавать допустимые символы в любой из уязвимых кодировок, где они ранее не существовали (т. Е. 0xbf27
по-прежнему 0xbf27
и т. Д.) - поэтому сервер все равно отклонит строку как недопустимую. Однако, смотрите @ eggyal's answer о другой уязвимости, которая может возникнуть при использовании этого режима SQL (хотя и не с PDO).
Безопасные примеры
Следующие примеры безопасны:
mysql_query('SET NAMES utf8');
$var = mysql_real_escape_string("\xbf\x27 OR 1=1 /*");
mysql_query("SELECT * FROM test WHERE name = '$var' LIMIT 1");
Поскольку сервер ожидает utf8
...
mysql_set_charset('gbk');
$var = mysql_real_escape_string("\xbf\x27 OR 1=1 /*");
mysql_query("SELECT * FROM test WHERE name = '$var' LIMIT 1");
Поскольку мы правильно установили набор символов, чтобы клиент и сервер совпадали.
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$pdo->query('SET NAMES gbk');
$stmt = $pdo->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$stmt->execute(array("\xbf\x27 OR 1=1 /*"));
Потому что мы отключили эмулированные подготовленные заявления.
$pdo = new PDO('mysql:host=localhost;dbname=testdb;charset=gbk', $user, $password);
$stmt = $pdo->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$stmt->execute(array("\xbf\x27 OR 1=1 /*"));
Потому что мы правильно установили набор символов.
$mysqli->query('SET NAMES gbk');
$stmt = $mysqli->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$param = "\xbf\x27 OR 1=1 /*";
$stmt->bind_param('s', $param);
$stmt->execute();
Потому что MySQLi постоянно делает действительно подготовленные операторы.
Завершение
Если вы:
- Использовать современные версии MySQL (последняя версия 5.1, все версии 5.5, 5.6 и т. Д.) И Параметр кодировки DSN PDO (в PHP ≥ 5.3.6)
OR
- Не используйте уязвимый набор символов для кодирования соединения (вы используете только
utf8
/ latin1
/ ascii
/ etc)
OR
- Включить
NO_BACKSLASH_ESCAPES
Режим SQL
Вы на 100% в безопасности.
В противном случае вы уязвимы , даже если вы используете подготовленные операторы PDO ...
Добавление
Я медленно работал над патчем, чтобы изменить настройки по умолчанию, чтобы они не эмулировали подготовку к будущей версии PHP. Проблема, с которой я сталкиваюсь, состоит в том, что МНОГО тестов ломаются, когда я делаю это. Одна из проблем заключается в том, что эмулированная подготовка будет генерировать только синтаксические ошибки при выполнении, но истинная подготовка будет вызывать ошибки при подготовке. Так что это может вызвать проблемы (и это одна из причин, по которым тесты не работают).