Достаточно ли подготовленных операторов PDO для предотвращения внедрения SQL?

585

Скажем, у меня такой код:

$dbh = new PDO("blahblah");

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

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

Параметры для подготовленных операторов не обязательно должны быть указаны; драйвер обрабатывает его для вас.

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

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

Теги:
security
sql-injection
pdo

7 ответов

659
Лучший ответ

Короткий ответ НЕТ, подготовка PDO не защитит вас от всех возможных атак SQL-Injection. Для некоторых неясных краевых случаев.

Я адаптирую этот ответ, чтобы поговорить о 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 здесь. Это устанавливает набор символов НА СЕРВЕРЕ. Есть еще один способ сделать это, но мы скоро доберемся.

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

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

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

  • $stmt- > Execute()

    Важно понять, что PDO по умолчанию делает NOT истинно подготовленные операторы. Он имитирует их (для MySQL). Поэтому PDO внутренне строит строку запроса, вызывая mysql_real_escape_string() (функцию API MySQL C) для каждого значения связанной строки.

    Запрос 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 будет молча backback для эмуляции утверждений, которые MySQL не может подготовить изначально: те, которые могут быть в руководстве, но будьте осторожны, чтобы выбрать соответствующую версию сервера).

Правильное исправление

Проблема в том, что мы не вызывали C API mysql_set_charset() вместо SET NAMES. Если бы мы это сделали, нам было бы хорошо, если бы мы использовали выпуск MySQL с 2006 года.

Если вы используете более раннюю версию MySQL, то bug в mysql_real_escape_string() означает, что недопустимые многобайтовые символы, например, в наша полезная нагрузка была обработана как одиночные байты для экранирования, даже если клиент был правильно проинформирован о кодировке соединения, и поэтому эта атака все равно будет успешной. Ошибка была исправлена ​​в MySQL 4.1.20, 5.0.22 и 5.1.11.

Но худшая часть заключается в том, что PDO не показывал C API для mysql_set_charset() до 5.3.6, поэтому в предыдущих версиях он не могпредотвратите эту атаку для каждой возможной команды!  Теперь он отображается как параметр utf8mb4 не уязвим и все же может поддерживать каждый символ Юникода: так что вы можете использовать его вместо этого, но он доступен только после того, как MySQL 5.5.3. Альтернативой является utf8, который также не уязвим и может поддерживать весь Unicode Основная многоязычная плоскость.

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

ИЛИ

  • Не используйте уязвимый набор символов для кодирования соединения (вы используете только utf8/latin1/ascii/etc)

ИЛИ

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

Вы на 100% безопасны.

В противном случае вы уязвимы , хотя вы используете подготовленные сообщения PDO...

Добавление

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

  • 41
    Это лучший ответ, который я нашел .. Можете ли вы предоставить ссылку для получения дополнительной ссылки?
  • 0
    Я думаю, что "Спасительная грация" - это исправление MySQL 5.0.22: dev.mysql.com/doc/relnotes/mysql/5.0/en/news-5-0-22.html
Показать ещё 15 комментариев
519

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

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

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

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

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

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


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

  • 6
    Это интересно. Я не знал о 1-м порядке против 2-го порядка. Не могли бы вы подробнее рассказать о том, как работает 2-й порядок?
  • 188
    Если ВСЕ ваши запросы параметризованы, вы также защищены от внедрения 2-го порядка. Внедрение 1-го порядка забывает, что пользовательские данные ненадежны. Внедрение 2-го порядка забывает, что данные базы данных ненадежны (потому что они изначально были получены от пользователя).
Показать ещё 15 комментариев
38

Нет, они не всегда.

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

$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 (Data Definition Language), то есть это не работает:

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

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

  • 10
    Я что-то здесь упускаю, но не в этом ли смысл готовых операторов, чтобы не рассматривать SQL как строку? Не будет что-то вроде $ dbh-> prepare ('SELECT * FROM: tableToUse, где username =: username'); обойти вашу проблему?
  • 3
    @RobForrest да, ты скучаешь :). Связанные данные работают только для DDL (язык определения данных). Вам нужны эти цитаты и правильное побег. Размещение кавычек для других частей запроса с большой вероятностью нарушает его. Например, SELECT * FROM 'table' может быть неправильным, так как это должен быть SELECT * FROM `table` или без каких-либо обратных ссылок. Тогда некоторые вещи, такие как ORDER BY DESC откуда DESC исходит от пользователя, не могут быть просто экранированы. Таким образом, практические сценарии довольно безграничны.
Показать ещё 5 комментариев
24

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

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

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

  • 10
    «Никогда» не способ завышения его, к точке бытия в заблуждении. Если вы используете подготовленные заявления неправильно, это не намного лучше, чем не использовать их вообще. (Конечно, «подготовленный оператор», в который введен пользовательский ввод, отрицательно сказывается на цели ... но я на самом деле видел, как это было сделано. И подготовленные операторы не могут обрабатывать идентификаторы (имена таблиц и т. Д.) В качестве параметров.) Добавить к тому же, некоторые драйверы PDO эмулируют подготовленные операторы, и для них есть место сделать это неправильно (например, путем тщательного анализа SQL). Короткая версия: никогда не думайте, что это так просто.
23

Нет, этого недостаточно (в некоторых конкретных случаях)! По умолчанию 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() снова с правильным кодированием и типом цитирования.

9

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

-2

Eaven, если вы собираетесь предотвращать внедрение SQL-инъекций, используя проверки html или js, вам следует учитывать, что передние проверки "обходятся".

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

Таким образом, чтобы предотвратить инъекцию SQL, было бы правильным дезинфицировать исходную базу данных даты внутри вашего контроллера.

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

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

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

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

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

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

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

  • 3
    Вы путаете SQL-инъекцию с чем-то другим, что делает ваш ответ совершенно неуместным

Ещё вопросы

Сообщество Overcoder
Наверх
Меню