Краткий ответ - НЕТ , 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
. Как выясняется, есть 5 таких кодировок , поддерживаемых в MySQL 5.6 по умолчанию: big5
, cp932
, gb2312
, gbk
и sjis
. Мы выберем gbk
здесь.
Теперь очень важно отметить использование SET NAMES
здесь. Это устанавливает набор символов на сервере . Есть еще один способ сделать это, но мы скоро туда доберемся.
Полезная нагрузка
Полезная нагрузка, которую мы собираемся использовать для этой инъекции, начинается с последовательности байтов 0xbf27
. Во- gbk
первых, это недопустимый многобайтовый символ; в latin1
это строка ¿'
. Обратите внимание, что в latin1
и gbk
, 0x27
само по себе является буквальным '
символом.
Мы выбрали эту полезную нагрузку, потому что, если бы мы ее вызвали addslashes()
, мы вставили бы ASCII, \
т.е. 0x5c
перед '
символом. Таким образом, мы получим 0xbf5c27
, что gbk
представляет собой последовательность из двух символов: с 0xbf5c
последующим 0x27
. Или, другими словами, действительный символ, за которым следует неоткрытый '
. Но мы не используем addslashes()
. Итак, к следующему шагу ...
$ Stmt-> Execute ()
Здесь важно понять, что PDO по умолчанию НЕ делает правильно подготовленные операторы. Он имитирует их (для MySQL). Поэтому PDO внутренне строит строку запроса, вызывая mysql_real_escape_string()
(функцию MySQL C API) для каждого значения связанной строки.
Вызов C API 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 не может подготовить изначально: те , которые можно будут перечислены в руководстве, но будьте осторожны , чтобы выбрать подходящую версию сервера).
Правильное Исправление
Проблема здесь в том, что мы не вызывали C API mysql_set_charset()
вместо SET NAMES
. Если бы мы это сделали, мы были бы в порядке, если бы использовали версию MySQL с 2006 года.
Если вы используете более раннюю версию MySQL, затем ошибку в mysql_real_escape_string()
виде , что недопустимые символы многобайтовых , такие как в наших полезных нагрузках рассматривались как отдельные байты для побега целей , даже если клиент был правильно информирован о кодировании соединения и поэтому эта атака будет все еще удастся Ошибка была исправлена в MySQL 4.1.20 , 5.0.22 и 5.1.11 .
Но хуже всего то, PDO
что не раскрывал C API mysql_set_charset()
до 5.3.6, поэтому в предыдущих версиях он не мог предотвратить эту атаку для всех возможных команд! Это теперь выставлено как параметр DSN , который должен использоваться вместо SET NAMES
...
Спасительная Грация
Как мы уже говорили, чтобы атака работала, соединение с базой данных должно быть закодировано с использованием уязвимого набора символов. utf8mb4
это не уязвима , и все же может поддерживать каждый символ Unicode: чтобы вы могли выбрать для использования , что вместо, но он был доступен только начиная с MySQL 5.5.3. Альтернатива utf8
, которая также не уязвима и может поддерживать всю базовую многоязычную плоскость Unicode .
Кроме того, вы можете включить NO_BACKSLASH_ESCAPES
режим SQL, который (среди прочего) изменяет работу mysql_real_escape_string()
. Если этот режим включен, 0x27
он будет заменен на, 0x2727
а не, 0x5c27
и, таким образом, процесс выхода не может создавать допустимые символы в любой из уязвимых кодировок, где они ранее не существовали (то 0xbf27
есть все еще и 0xbf27
т. Д.), Поэтому сервер все равно отклонит строку как недопустимую. , Однако см . Ответ @ eggyal о другой уязвимости, которая может возникнуть при использовании этого режима 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)
ИЛИ
- Не используйте уязвимый набор символов для кодирования соединения (вы используете только
utf8
/ latin1
/ ascii
/ etc)
ИЛИ
- Включить
NO_BACKSLASH_ESCAPES
режим SQL
Вы на 100% в безопасности.
В противном случае вы уязвимы, даже если вы используете подготовленные операторы PDO ...
добавление
Я медленно работал над патчем, чтобы изменить настройки по умолчанию, чтобы они не эмулировали подготовку к будущей версии PHP. Проблема, с которой я сталкиваюсь, состоит в том, что МНОГО тестов ломаются, когда я делаю это. Одна из проблем заключается в том, что эмулированная подготовка будет генерировать только синтаксические ошибки при выполнении, но истинная подготовка будет вызывать ошибки при подготовке. Так что это может вызвать проблемы (и это одна из причин, по которой тесты не работают).