Достаточны ли подготовленные выражения PDO для предотвращения SQL-инъекций?
Я сталкиваюсь с проблемой, связанной с безопасностью при работе с базами данных в PHP с использованием PDO. У меня есть следующий код:
$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 ответ(ов)
Подготовленные выражения и параметризованные запросы достаточно для предотвращения SQL-инъекций, но только при условии их использования на протяжении всего приложения для каждого запроса.
Если вы где-то в приложении используете неконтролируемый динамический SQL, это всё равно создаёт уязвимость для инъекций второго порядка.
Инъекция второго порядка означает, что данные были несколько раз обработаны в базе данных перед тем, как они были включены в запрос, и её гораздо сложнее осуществить. Насколько мне известно, реальные случаи инъекций второго порядка встречаются крайне редко, так как злоумышленникам обычно проще использовать социальную инженерию, чтобы попасть в систему. Тем не менее, иногда могут возникать ошибки второго порядка из-за лишних безобидных символов, таких как '
.
Вы можете осуществить атаку инъекцией второго порядка, если сможете сохранить значение в базе данных, которое позже будет использовано как литерал в запросе. Например, предположим, что вы вводите следующую информацию в качестве вашего нового имени пользователя при создании учетной записи на веб-сайте (предполагая, что используется баз данных MySQL):
' + (SELECT UserName + '_' + Password FROM Users LIMIT 1) + '
Если на имя пользователя нет никаких других ограничений, подготовленное выражение всё равно гарантирующим, что вложенный запрос не выполнится во время вставки и значение будет корректно сохранено в базе данных. Однако, представьте, что позже приложение извлекает ваше имя пользователя из базы данных и использует конкатенацию строк, чтобы включить это значение в новый запрос. Вы можете увидеть чей-то чужой пароль. Поскольку первыми именами в таблице пользователей чаще всего являются администраторы, вы могли бы ещё и раскрыть доступ к админской панели. (Также стоит отметить: это ещё одна причина не хранить пароли в открытом виде!)
Таким образом, мы понимаем, что если подготовленные выражения используются только для единичного запроса, а для остальных запросов игнорируются, то они не защищают от SQL-инъекций на уровне всего приложения, потому что им не хватает механизма, который обеспечивал бы использование безопасного кода для всех операций доступа к базе данных в приложении. Однако, если придерживаться хорошего проектирования приложений — что может включать практики, такие как рецензирование кода, статический анализ или использование ORM, слоя данных или сервисного слоя, который ограничивает динамический SQL — подготовленные выражения становятся основным инструментом для решения проблемы SQL-инъекций. Если вы следуете хорошим принципам проектирования приложений, так что доступ к данным отделён от остальной логики программы, это становится проще обеспечить или проверить, что каждый запрос правильно использует параметризацию. В таком случае, SQL-инъекция (как первого, так и второго порядка) полностью предотвращается.
Нет, они не всегда безопасны.
Это зависит от того, позволяете ли вы пользователям вводить данные, которые будут использоваться в самом запросе. Например:
$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.
Лично я всегда предпочитаю сначала проводить некоторую санитацию данных, так как нельзя доверять пользовательскому вводу. Однако, при использовании заполнителей (placeholders) или связывания параметров (parameter binding) вводимые данные отправляются на сервер отдельно от SQL-запроса, а затем связываются вместе. Главное здесь в том, что это связывает предоставленные данные с конкретным типом и конкретным использованием, тем самым устраняя любую возможность изменения логики SQL-запроса.
Даже если вы планируете предотвратить SQL-инъекции на стороне клиента с помощью проверок в HTML или JS, нужно учитывать, что такие проверки можно обойти.
Вы можете отключить JavaScript или изменить шаблон с помощью инструмента разработки, встроенного в современные браузеры, такие как Firefox или Chrome.
Поэтому для предотвращения SQL-инъекций правильным решением будет санировать входные данные на сервере в вашем контроллере.
Рекомендую использовать встроенную функцию PHP filter_input()
для очистки значений, получаемых через GET и INPUT.
Если вы хотите повысить уровень безопасности для ответственных операций с базой данных, я советую использовать регулярные выражения для валидации формата данных. В этом случае вам поможет preg_match()
. Но будьте осторожны! Регулярные выражения могут быть ресурсозатратными. Используйте их только в случае необходимости, иначе производительность вашего приложения может пострадать.
Простой пример:
Если вам нужно дважды проверить, является ли значение, полученное через GET, числом меньше 99:
if (!preg_match('/[0-9]{1,2}/', $value)) {...}
Это будет тяжелее, чем:
if (isset($value) && intval($value) < 99) {...}
Итак, окончательный ответ таков: "Нет! Подготовленные выражения PDO не предотвращают все виды SQL-инъекций"; они не защищают от неожиданных значений, а лишь от неожиданных конкатенаций.
Как предотвратить SQL-инъекции в PHP?
Как санировать пользовательский ввод с помощью PHP?
Почему не стоит использовать функции mysql_* в PHP?
Можно ли привязать массив к условию IN() в запросе PDO?
Что такое потокобезопасность и непотокобезопасность в PHP?