Являются ли подготовленные 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 ответов


короткий ответ:нет, 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() (функция 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 не выставлял API C для mysql_set_charset() до 5.3.6, поэтому в предыдущих версиях это не может предотвратите эту атаку для каждой возможной команды! Теперь он выставлен как параметр DSN, который следует использовать вместо SET NAMES...

Спасительная Благодать

как мы уже говорили в начале, для этой атаки для работы соединение с базой данных должно быть закодировано с использованием уязвимого набора символов. utf8mb4 is не уязвимых и все же может поддерживать символ Unicode: поэтому вы могли бы использовать это вместо этого-но он был доступен только с 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 PDO (в PHP ≥ 5.3.6)

или

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

или

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

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

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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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