Достаточно ли подготовленных операторов PDO для предотвращения внедрения SQL? - PullRequest
608 голосов
/ 25 сентября 2008

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

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

749 голосов
/ 30 августа 2012

Краткий ответ: НЕТ , подготовка к работе с 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 здесь. Это устанавливает набор символов ON SERVER . Есть еще один способ сделать это, но мы скоро туда доберемся.

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

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

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

  3. $ stmt-> Execute ()

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

Благодать

Как мы уже говорили, чтобы атака работала, соединение с базой данных должно быть закодировано с использованием уязвимого набора символов. utf8mb4 является не уязвимым и все же может поддерживать каждый символ Unicode: так что вы можете использовать его вместо & mdash; но он был доступен только с MySQL 5.5 0,3. Альтернатива - utf8, которая также не уязвима и может поддерживать весь Unicode Базовая многоязычная плоскость .

В качестве альтернативы, вы можете включить режим SQL NO_BACKSLASH_ESCAPES, который (среди прочего) изменяет работу mysql_real_escape_string(). Если этот режим включен, 0x27 будет заменен на 0x2727, а не 0x5c27, и, таким образом, экранирующий процесс не может создавать допустимые символы в любой из уязвимых кодировок, где они ранее не существовали (т. Е. 0xbf27 по-прежнему 0xbf27 и т. Д.) - поэтому сервер все равно отклонит строку как недопустимую. Однако, смотрите @ eggyal's answer о другой уязвимости, которая может возникнуть при использовании этого режима 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)

OR

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

OR

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

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

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

Добавление

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

503 голосов
/ 25 сентября 2008

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

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

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

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

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

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


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

39 голосов
/ 21 апреля 2010

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

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

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

24 голосов
/ 30 августа 2012

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

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

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

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

9 голосов
/ 25 сентября 2008

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

0 голосов
/ 04 марта 2018

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

...