Достаточно ли подготовленных операторов 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, Как оказалось, в 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-> Execute()

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

    Вызов 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 не подвергать 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 / так далее)

ИЛИ ЖЕ

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

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

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

добавление

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

Подготовленных операторов / параметризованных запросов обычно достаточно, чтобы предотвратить внедрение 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 (хорошо, были) просто глупы в обработке параметров, когда задействованы широкие символы, и все еще есть редкий случай, описанный в другом ответе с высоким числом голосов, который может позволить инъекции проскользнуть через параметризованный запрос.

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

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

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

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

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

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

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

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

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

Другие вопросы по тегам