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

7 567

7 ответов:

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

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

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

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

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

  2. Груз

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

    мы выбрали эту полезную нагрузку, потому что, если мы позвонили addslashes() на нем мы бы вставили ASCII \ т. е. 0x5c до ' символ. Так что мы закончим с 0xbf5c27, который в gbk представляет собой последовательность из двух символов:0xbf5c следовал по 0x27. Или в другом слова, а действительный символ, за которым следует unescaped '. Но мы не используем 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 набор символов, мы увидим:
    縗' OR 1=1 /*

    именно этого и требует атака.

  4. Запрос

    эта часть-просто формальность, но вот визуализация запрос:

    SELECT * FROM test WHERE name = '縗' OR 1=1 /*' LIMIT 1
    

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

Простое Исправление

теперь стоит отметить, что вы можете предотвратить это, отключив эмулированные подготовленные операторы:

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

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

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

проблема здесь в том, что мы не вызывали 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 etc.)- таким образом, сервер все равно отклонит строку как недопустимую. Однако смотрите @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 charset PDO (в PHP ≥ 5.3.6)

или

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

или

  • включить NO_BACKSLASH_ESCAPES режим SQL

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

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

дополнительное соглашение

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

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

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

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

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

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

мы видим, что подготовленных операторов достаточно для одного запроса, но сами по себе они не достаточно для защиты от атак SQL-инъекций во всем приложении, поскольку у них нет механизма для обеспечения того, чтобы весь доступ к базе данных в приложении использовал безопасный код. Однако, использованный как часть хорошего проектирование приложений-которое может включать такие методы, как просмотр кода или статический анализ, или использование ORM, уровня данных или уровня сервиса, который ограничивает динамический sql - подготовленные заявления are основной инструмент для решения проблемы инъекции 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 injection front-end, используя проверки html или js, вам нужно будет учитывать, что проверки переднего плана "обходимы".

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

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

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

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

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

легко пример:

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

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

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