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

660

Допустим, у меня есть такой код:

$dbh = new PDO("blahblah");

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

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

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

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

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

Марк Бик
источник
5
лучше подойти к
седьмому

Ответы:

808

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

Спасительная Грация

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

ИЛИ

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

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

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

добавление

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

ircmaxell
источник
47
Это лучший ответ, который я нашел .. Можете ли вы предоставить ссылку для получения дополнительной ссылки?
StaticVariable
1
@nicogawenda: это была другая ошибка. До 5.0.22 mysql_real_escape_stringнеправильно обрабатывать случаи, когда соединение было правильно установлено на BIG5 / GBK. Таким образом, даже вызов mysql_set_charset()mysql <5.0.22 будет уязвим для этой ошибки! Так что нет, этот пост все еще применим к 5.0.22 (потому что mysql_real_escape_string является только кодировкой от вызовов mysql_set_charset(), о чем этот пост говорит об обходе) ...
ircmaxell
1
@progfa Независимо от того, делает это или нет, вы всегда должны проверять свой ввод на сервере, прежде чем делать что-либо с пользовательскими данными.
Tek
2
Обратите внимание, что NO_BACKSLASH_ESCAPESтакже могут
появиться
2
@slevin "OR 1 = 1" - это место для всего, что вы хотите. Да, он ищет значение в имени, но представьте, что часть «ИЛИ 1 = 1» была «UNION SELECT * FROM users». Теперь вы управляете запросом и,
следовательно
515

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

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

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

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

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

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


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

Джоэл Коухорн
источник
6
Это интересно. Я не знал о 1-м порядке против 2-го порядка. Не могли бы вы подробнее рассказать о том, как работает 2-й порядок?
Марк Биек
193
Если ВСЕ ваши запросы параметризованы, вы также защищены от внедрения 2-го порядка. Внедрение 1-го порядка забывает, что пользовательские данные ненадежны. Внедрение 2-го порядка забывает, что данные базы данных ненадежны (потому что они изначально были получены от пользователя).
CJM
6
Спасибо CJM. Я также нашел эту статью полезной для объяснения инъекций 2-го порядка: codeproject.com/KB/database/SqlInjectionAttacks.aspx
Марк Бик,
49
О да. Но как насчет инъекции третьего порядка . Должны быть в курсе тех.
troelskn
81
@troelskn, где разработчик является источником ненадежных данных
MikeMurko
45

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

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

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

башня
источник
11
Я что-то здесь упускаю, но не в этом ли смысл подготовленных операторов, чтобы не рассматривать SQL как строку? Не будет что-то вроде $ dbh-> prepare ('SELECT * FROM: tableToUse, где username =: username'); обойти вашу проблему?
Роб Форрест
4
@RobForrest да, ты скучаешь :). Связываемые данные работают только для DDL (язык определения данных). Вам нужны эти цитаты и правильное побег. Размещение кавычек для других частей запроса с большой вероятностью нарушает его. Например, SELECT * FROM 'table'может быть не так, как должно быть, SELECT * FROM `table`или без каких-либо помех. Тогда некоторые вещи, например, ORDER BY DESCоткуда DESCисходит пользователь, не могут быть просто исключены. Таким образом, практические сценарии довольно безграничны.
Башня
8
Интересно, как 6 человек могли поднять комментарий, предлагающий явно неправильное использование подготовленного утверждения. Если бы они даже попробовали это однажды, они бы сразу обнаружили, что использование именованного параметра вместо имени таблицы не будет работать.
Феликс Ганьон-Гренье
Вот отличный учебник по PDO, если вы хотите его изучить. a2znotes.blogspot.in/2014/09/introduction-to-pdo.html
Р.Н. Кушваха,
11
Вы никогда не должны использовать строку запроса / тело POST, чтобы выбрать таблицу для использования. Если у вас нет моделей, по крайней мере используйте switchдля получения имени таблицы.
ZiggyTheHamster,
29

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

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

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

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

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

PeeHaa
источник
14

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

JimmyJ
источник
1

Еще раз, если вы собираетесь предотвратить внедрение 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»; Это не предотвращает неожиданные значения, просто неожиданное объединение

snipershady
источник
5
Вы путаете SQL-инъекцию с чем-то еще, что делает ваш ответ совершенно неуместным
ваш здравый смысл