Достаточны ли подготовленные PDO инструкции для предотвращения SQL-инъекции?

StackOverflow https://stackoverflow.com/questions/134099

Вопрос

Допустим, у меня есть такой код, как этот:

$dbh = new PDO("blahblah");

$stmt = $dbh->prepare('SELECT * FROM users where username = :username');
$stmt->execute( array(':username' => $_REQUEST['username']) );

В документации PDO говорится:

Параметры подготовленных инструкций не нужно заключать в кавычки;водитель сделает это за вас.

Это действительно все, что мне нужно сделать, чтобы избежать SQL-инъекций?Неужели это действительно так просто?

Вы можете использовать MySQL, если это имеет значение.Кроме того, мне действительно интересно только использование подготовленных инструкций против SQL-инъекции.В этом контексте меня не волнуют XSS или другие возможные уязвимости.

Это было полезно?

Решение

Короткий ответ таков НЕТ, PDO prepares не защитит вас от всех возможных атак с использованием 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 строки.Давайте проанализируем, что здесь происходит:

  1. Выбор набора символов

    $pdo->query('SET NAMES gbk');
    

    Чтобы эта атака сработала, нам нужна кодировка, которую сервер ожидает от соединения для кодирования ' как в ASCII , т. е. 0x27 и иметь некоторый символ, конечным байтом которого является ASCII \ т. е. 0x5c.Как оказалось, в MySQL 5.6 по умолчанию поддерживается 5 таких кодировок: big5, cp932, gb2312, gbk и sjis.Мы выберем gbk вот.

    Теперь очень важно отметить использование SET NAMES вот.Это задает набор символов НА СЕРВЕРЕ.Есть и другой способ сделать это, но мы доберемся до него достаточно скоро.

  2. Полезная нагрузка

    Полезная нагрузка, которую мы собираемся использовать для этого внедрения, начинается с последовательности байтов 0xbf27gbk, это недопустимый многобайтовый символ;в latin1, это строка ¿'.Обратите внимание , что в latin1 и gbk, 0x27 само по себе это буквальное ' характер.

    Мы выбрали эту полезную нагрузку, потому что, если бы мы вызвали addslashes() на нем мы бы вставили ASCII \ т. е. 0x5c, перед началом ' характер.Так что в итоге мы получили бы 0xbf5c27, который в gbk представляет собой последовательность из двух символов: 0xbf5c за которым следует 0x27.Или, другими словами, действительный символ, за которым следует неэкранированный '.Но мы не используем addslashes().Итак, переходим к следующему шагу...

  3. $stmt->выполнить()

    Здесь важно понимать, что 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 /*

    Это именно то, что требуется для атаки.

  4. Запрос

    Эта часть - всего лишь формальность, но вот отрисованный запрос:

    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 является не уязвимый и все же может поддержать каждый Символ Юникода:таким образом, вы могли бы использовать это вместо этого, но это было доступно только начиная с MySQL 5.5.3.Альтернативой является utf8, который также не уязвимый и может поддерживать весь Unicode Базовый Многоязычный самолет.

В качестве альтернативы, вы можете включить NO_BACKSLASH_ESCAPES Режим SQL, который (среди прочего) изменяет работу mysql_real_escape_string().При включенном этом режиме, 0x27 будет заменен на 0x2727 вместо того , чтобы 0x5c27 и, таким образом, процесс побега не могу создайте допустимые символы в любой из уязвимых кодировок там, где они ранее не существовали (т. е. 0xbf27 все еще 0xbf27 и т.д.) — таким образом, сервер все равно отклонит строку как недопустимую.Однако, смотрите Ответ @eggyal's для другой уязвимости, которая может возникнуть при использовании этого режима 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 / и т.д.)

или

  • Включить NO_BACKSLASH_ESCAPES Режим SQL

Ты в безопасности на 100%.

В противном случае вы становитесь уязвимы даже если вы используете подготовленные инструкции PDO...

Добавление

Я медленно работал над исправлением, чтобы изменить значение по умолчанию на не эмулировать prepares для будущей версии PHP.Проблема, с которой я сталкиваюсь, заключается в том, что МНОГИЕ тесты ломаются, когда я это делаю.Одна из проблем заключается в том, что эмулируемые prepares будут выдавать только синтаксические ошибки при выполнении, но истинные prepares будут выдавать ошибки при подготовке.Так что это может вызвать проблемы (и является одной из причин, по которой тесты не выполняются).

Другие советы

Подготовленных инструкций / параметризованных запросов, как правило, достаточно для предотвращения 1 - й порядок комментарий к этому утверждению*.Если вы используете непроверенный динамический sql где-либо еще в своем приложении, вы по-прежнему уязвимы для 2 - й порядок инъекция.

внедрение 2-го порядка означает, что данные были циклически обработаны через базу данных один раз, прежде чем быть включенными в запрос, и их гораздо сложнее выполнить.AFAIK, вы почти никогда не видите реальных инженерных атак 2-го порядка, поскольку злоумышленникам обычно проще внедриться в социальную инженерию, но иногда возникают ошибки 2-го порядка из-за дополнительных неопасных ' персонажи или что-то подобное.

Вы можете выполнить атаку с использованием инъекций 2-го порядка, когда вы можете сохранить значение в базе данных, которое позже используется в качестве литерала в запросе.В качестве примера предположим, что вы вводите следующую информацию в качестве своего нового имени пользователя при создании учетной записи на веб-сайте (предполагая, что для этого вопроса используется база данных MySQL):

' + (SELECT UserName + '_' + Password FROM Users LIMIT 1) + '

Если нет других ограничений на имя пользователя, подготовленная инструкция все равно будет следить за тем, чтобы приведенный выше встроенный запрос не выполнялся во время вставки, и правильно сохранять значение в базе данных.Однако представьте, что позже приложение извлекает ваше имя пользователя из базы данных и использует конкатенацию строк, чтобы включить это значение в новый запрос.Возможно, вы увидите чужой пароль.Поскольку первые несколько имен в таблице users, как правило, являются администраторами, возможно, вы также просто отдали ферму.(Также обратите внимание:это еще одна причина не хранить пароли в виде обычного текста!)

Таким образом, мы видим, что подготовленных операторов достаточно для одного запроса, но сами по себе они нет достаточный для защиты от атак с использованием sql-инъекций во всем приложении, поскольку в них отсутствует механизм обеспечения того, чтобы при любом доступе к базе данных в приложении использовался безопасный код.Однако используется как часть хорошего дизайна приложения, который может включать такие практики, как проверка кода или статический анализ, или использование ORM, уровня данных или сервисного уровня, которые ограничивают динамический sql — подготовленные заявления являются основной инструмент для решения проблемы Sql-инъекции. Если вы следуете хорошим принципам проектирования приложений, таким образом, что ваш доступ к данным отделен от остальной части вашей программы, становится легко обеспечить соблюдение или аудит того, что каждый запрос правильно использует параметризацию.В этом случае sql-инъекция (как первого, так и второго порядка) полностью предотвращается.


*Оказывается, что MySQL / PHP просто тупы в обработке параметров, когда задействованы широкие символы, и все еще существует Редкий случай, описанный в другой высоко оцененный ответ здесь это может позволить внедрению проскользнуть через параметризованный запрос.

Нет, так бывает не всегда.

Это зависит от того, разрешаете ли вы размещать пользовательский ввод внутри самого запроса.Например:

$dbh = new PDO("blahblah");

$tableToUse = $_GET['userTable'];

$stmt = $dbh->prepare('SELECT * FROM ' . $tableToUse . ' where username = :username');
$stmt->execute( array(':username' => $_REQUEST['username']) );

было бы уязвимо для SQL-инъекций, и использование подготовленных инструкций в этом примере не сработает, поскольку пользовательский ввод используется как идентификатор, а не как данные.Правильным ответом здесь было бы использовать какую-то фильтрацию / проверку, например:

$dbh = new PDO("blahblah");

$tableToUse = $_GET['userTable'];
$allowedTables = array('users','admins','moderators');
if (!in_array($tableToUse,$allowedTables))    
 $tableToUse = 'users';

$stmt = $dbh->prepare('SELECT * FROM ' . $tableToUse . ' where username = :username');
$stmt->execute( array(':username' => $_REQUEST['username']) );

Примечание:вы не можете использовать PDO для привязки данных, которые выходят за пределы DDL (языка определения данных), т.е.это не сработает:

$stmt = $dbh->prepare('SELECT * FROM foo ORDER BY :userSuppliedData');

Причина, по которой вышеприведенное не работает, заключается в том, что DESC и ASC не являются данные.PDO может убежать только для данные.Во-вторых, вы даже не можете поместить ' кавычки вокруг него.Единственный способ разрешить выбранную пользователем сортировку - это вручную отфильтровать и проверить, что это либо DESC или ASC.

Да, этого достаточно.Способ, которым работают атаки типа инъекции, заключается в том, что каким-то образом интерпретатор (база данных) оценивает что-то, что должно было быть данными, как если бы это был код.Это возможно только в том случае, если вы смешиваете код и данные на одном носителе (например.когда вы создаете запрос в виде строки).

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

Тем не менее, вы все еще можете быть уязвимы для других атак типа инъекций.Например, если вы используете данные на HTML-странице, вы можете подвергнуться атакам типа XSS.

Нет, этого недостаточно (в некоторых конкретных случаях)!По умолчанию PDO использует эмулированные подготовленные инструкции при использовании MySQL в качестве драйвера базы данных.Вы всегда должны отключать эмулируемые подготовленные инструкции при использовании MySQL и PDO:

$dbh->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);

Еще одна вещь, которую всегда следует делать, это установить правильную кодировку базы данных:

$dbh = new PDO('mysql:dbname=dbtest;host=127.0.0.1;charset=utf8', 'user', 'pass');

Также смотрите этот связанный с этим вопрос: Как я могу предотвратить внедрение SQL в PHP?

Также обратите внимание, что это касается только части базы данных, за которой вам все равно придется следить самостоятельно при отображении данных.Например.используя htmlspecialchars() опять же, с правильной кодировкой и стилем цитирования.

Лично я бы всегда сначала запускал какую-либо форму очистки данных, поскольку вы никогда не можете доверять вводимым пользователем данным, однако при использовании привязки заполнителей / параметров введенные данные отправляются на сервер отдельно к инструкции sql, а затем связываются вместе.Ключевым моментом здесь является то, что это привязывает предоставленные данные к определенному типу и конкретному использованию и исключает любую возможность изменить логику инструкции SQL.

Eaven если вы собираетесь предотвратить интерфейсное внедрение sql-кода, используя проверки html или js, вам следует учитывать, что интерфейсные проверки "можно обойти".

Вы можете отключить js или отредактировать шаблон с помощью интерфейсного инструмента разработки (в настоящее время встроен в firefox или Chrome).

Итак, чтобы предотвратить SQL-инъекцию, было бы правильно очистить серверную часть даты ввода внутри вашего контроллера.

Я хотел бы предложить вам использовать встроенную функцию filter_input() PHP для очистки GET и входных значений.

Если вы хотите повысить безопасность для разумных запросов к базе данных, я хотел бы предложить вам использовать регулярное выражение для проверки формата данных.preg_match() поможет вам в этом случае!Но будь осторожен!Движок регулярных выражений не такой легкий.Используйте его только в случае необходимости, в противном случае производительность вашего приложения снизится.

Безопасность требует определенных затрат, но не тратьте впустую свою производительность!

Простой пример:

если вы хотите дважды проверить, является ли значение, полученное из GET, числом, меньшим 99 if(!preg_match('/[0-9]{1,2}/')){...} является более тяжелым из

if (isset($value) && intval($value)) <99) {...}

Итак, окончательный ответ таков:"Нет!Подготовленные инструкции PDO не предотвращают все виды sql-инъекций ";Это не предотвращает неожиданные значения, просто неожиданную конкатенацию

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top