Являются ли подготовленные PDO заявления достаточными для предотвращения SQL-инъекций?

Предположим, у меня такой код:

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

Документация PDO гласит:

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

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

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

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

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

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

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

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

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


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

Короткий ответ NO , 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 строки. Давайте проанализируем, что здесь происходит:

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

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

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

    Теперь очень важно отметить использование SET NAMES здесь. Это устанавливает набор символов на сервере . Есть еще один способ сделать это, но мы скоро доберемся туда.

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

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

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

  3. $ stmt-> Execute ()

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

    Вызов C API для mysql_real_escape_string() отличается от addslashes() тем, что он знает набор символов соединения. Таким образом, он может правильно выполнить экранирование набора символов, ожидаемого сервером. Однако до этого момента клиент думает, что мы все еще используем latin1 для соединения, потому что мы никогда не говорили об этом иначе. Мы сказали серверу, что мы используем gbk , но клиент все еще думает, что это latin1 .

    Поэтому вызов mysql_real_escape_string() вставляет обратную косую черту, и у нас есть свободный висячий символ в нашем «экранированном» контенте! На самом деле, если бы мы посмотрели на $var в gbk символов 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 не может подготовить изначально: те, которые могут быть указаны в руководстве, но будьте осторожны, чтобы выбрать соответствующую версию сервера).

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

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

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

Но худшая часть заключается в том, что PDO не mysql_set_charset() API C для 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 для другой уязвимости, которая может возникнуть в результате использования этого режима 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. Проблема, с которой я сталкиваюсь, заключается в том, что при тестировании ломается множество тестов. Одна из проблем заключается в том, что эмулированные препараты будут только вызывать синтаксические ошибки при выполнении, но истинные готовые вызовут ошибки при подготовке. Таким образом, это может вызвать проблемы (и является частью теста причины).

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

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

 $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 .

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

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

Тем не менее, вы все равно можете быть уязвимы для других инъекций типа инъекций. Например, если вы используете данные на 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.