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

Author: Patrick Hofman, 2008-09-25

7 answers

Короткий ответ НЕТ, подготовка 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. Как оказалось, в MySQL 5.6 по умолчанию поддерживается 5 таких кодировок: big5, cp932, gb2312, gbk и sjis. Мы выберем gbk здесь.

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

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

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

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

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

    Важное здесь следует понимать, что PDO по умолчанию НЕ выполняет истинные подготовленные утверждения. Он эмулирует их (для 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 /*

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

  4. Тот Запрос

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

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

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

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

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

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

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

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

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

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

Но хуже всего то, что PDO не предоставлял 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 И т.д.) И Параметр кодировки DSN PDO (в PHP ≥5.3.6)

ИЛИ

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

ИЛИ

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

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

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

Добавление

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

 702
Author: ircmaxell, 2017-05-23 12:34:48

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

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

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

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

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

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


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

 496
Author: Joel Coehoorn, 2017-07-18 16:53:12

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

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

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

 40
Author: Tower, 2012-09-27 13:20:25

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

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

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

 24
Author: troelskn, 2008-09-25 15:55:46

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

 24
Author: PeeHaa, 2017-05-23 12:34:48

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

 9
Author: JimmyJ, 2008-09-25 15:50:43

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

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

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

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

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

Безопасность имеет свои издержки, но не тратьте впустую свою производительность!

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

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

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

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

 -2
Author: snipershady, 2018-03-04 20:17:56