Вопросы и ответы

Почему нельзя просто "проискейпить" все данные, прогнав их через mysql_real_escape_string()?
Почему нужно обязательно использовать плейсхолдеры?
Зачем нужны типизованные плейсхолдеры?
Почему ты не используешь "родные" плейсхолдеры базы данных?
Но если я хочу использовать родные?
В чем разница между подготовленными выражениями (prepared statements) и плейсхолдерами (placeholders)?
Почему у меня не работает LIKE?
Зачем работать с чистым SQL, ведь ORM лучше!
Комментарии (32)

Объясняя принципы безопасной работы с SQL на разных форумах, я понял, что некоторые вещи далеко не очевидны. поэтому я решил ответить здесь на самые актуальные

Почему нельзя просто "проискейпить" все данные, прогнав их через mysql_real_escape_string()?
Это самый важный вопрос, поскольку он показывает чудовищное заблуждение, бытующее среди РНР разработчиков.
Дело в том, что mysql_real_escape_string() не предназначена для защиты от инъекций. Вообще. Это функция, которая нужна для форматирования строк. Причём даже для этой задачи её одной недостаточно - форматирование включает в себя два действия - экранирование и заключение в кавычки. По отдельности они не имеют смысла. при этом строки надо всегда, независимо от любых условий, форматировать в соответствии с этими двумя правилами.
Следовательно, строки мы должны форматировать независимо от возможных инъекций.
А всё остальное искейпить попросту нет смысла - у них свои собственные правила форматирования. Именно поэтому мы всегда должны знать тип данных, попадающих в запрос.

Почему нужно обязательно использовать плейсхолдеры?
Во-первых, это безопаснее - поместив данные в запрос через плейсхолдер, мы можем быть уверены в том, что они будут правильно обработаны.
Во-вторых, все полагающиеся обработки делает программа - не нужно писать их каждый раз в коде самому.
В-третьих, всё полагающееся форматирование накладывается только на поступающее в запрос значение, но никак не затрагивает исходную переменную.

Зачем нужны типизованные плейсхолдеры?
Здесь две причины.
Во-первых, создатели родных подготовленных выражений исходили из совершенно дурацкого предположения, что в запрос динамически могут попадать только скалярные данные - строки и числа. Но в реальной жизни приходится в запрос подставлять ещё и идентификаторы (имена полей и таблиц) или целые блоки значений, например - массив для оператора IN(). Поддержки таких распространённых случаев встроенные библиотеки не дают, и нам её надо вводить самим. Получается, что в запрос могут попадать элементы множества типов, а не одного-двух.
И здесь мы подходим ко второй причине: удобство.
В родных подготовленных выражениях mysqli надо каждый плейсхолдер обрабатывать отдельно, указывая его тип. Код получается очень длинным, с огромным количесвтвом повторений. Но если внести в плейсхолдер всего лишь небольшое изменение - добавив его тип - то задача сразу упрощается, и все данные можно передать просто в параметрах функции, выполняющей запрос!

Почему ты не используешь "родные" плейсхолдеры базы данных?
Они же ещё и быстрее?

В первую очередь - по перечисленным выше причинам. Это самая главная причина.
Ещё одна причина - подставить их в запрос можно только все одновременно. Что часто бывает дико неудобно, если запрос составляется по частям по условиям.
По поводу же скорости - это попросту заблуждение.
Считается, что prepared запрос выполнится всего один раз, а потом можно будет делать execute сколько угодно. И много у вас в скриптах таких запросов? Тысячные доли процента. Остальные исполняются по одному разу. В итоге, фактически, мы получаем замедление, а не ускорение, поскольку скрипту приходится по два раза обращаться к БД на один запрос.

Но если я хочу использовать родные?
В принципе, сделать это нетрудно. Нужно всего лишь добавить в класс два новых метода, prepare() и execute() - обертки над методами mysqli, плюс подменить обработчики строкового и целочисленного плейсхолдеров.
Да - и ещё надо будет использовать отдельный оператор, чтобы перевести результат запроса в пригодный для использования вид.

В чем разница между подготовленными выражениями (prepared statements) и плейсхолдерами (placeholders)?
Разве это не одно и то же?

Нет. Плейсхолдер - понятие более широкое. Это "заместитель", представитель актуального выражения в строке. %d в printf - тоже плейсхолдер. Фишка плейсхолдера в том, что перед вставкой в строку мы его обрабатываем тем или иным образом.
Подготовленное выражение же - это частный случай плейсхолдера, обработкой значения которого занимается сервер БД. Переданные значения никогда не попадают в строку запроса, а обрабатываются базой отдельно. Сначала в базу посылается запрос без данных, только с плейсхолдерами (эта операция называется "prepare" - подготовить. Следовательно, сам запрос - "prepared statement"). А затем, отдельно от него, на сервер посылаются данные (execute).
Как можно увидеть, prepared statements в большинстве случаев усложняют задачу (делая по два обращения к базе на 1 запрос) и затрудняют отладку (мы не можем простыми средствами получить канонический SQL запрос, полученный из переданных данных, что бывает необходимо для визуального контроля и проверки в консоли).
В то время как плейсхолдеры, обрабатываемые на клиенте, лишены этих недостатков.

Почему у меня не работает LIKE?
Это очень часто встречающийся и очень хороший вопрос на понимание сути плейсхолдеров вообще и prepared statements в частности. Возникнуть он может только если рассматривать запрос с позиции "пихаем всё что под руку попало (и искейпим "вредные" символы)".
Если же рассматривать запрос по частям, отделяя данные от кода, то отличия есть.

Агументом оператора лайк является строковый литерал. У строкового литерала есть простые правила форматирования:
- он должен быть ограничен кавычками
- внутри должны быть прослешены спецсимволы.

Следовательно, если мы хотим делать всё правильно, мы должны помещать в запрос уже отформатированный литерал. Целиком.
А не собирать его в запросе. Потому что такая сборка в итоге и ведет к инъекциям.
Именно очень большая разница между  LIKE :placeholder и LIKE '%:placeholder%'
Первое - законный элемент синтаксиса. В режиме эмуляции будет трансформирован в строку в соответствии с вышеперечисленными правилами.
Второе - литерал, собранный из огрызков. Трансформировать в строку в соответствии с правилами невозможно

Соответственно, сначала собираем нужную нам строку, а потом подставляем её через плейсхолдер:
$data = $db->getAll("SELECT * FROM t WHERE f LIKE ?s","%$like%");

Но дело всё равно даже не в этом
А в том, что плейсхолдер не может являться частью литерала.
Плейсхолдер - это отдельный элемент синтаксиса запроса. Ну как знак "+" в ПХП.
Если он стоит просто в коде - то это оператор "плюс".
Но если он стоит внутри строки - это просто символ!
Так и здесь:
Если плейсхолдер является самостоятельной частью запроса - это плейсхолдер. Если же частью строки - это всего лишь часть строки безо всякого специального значения.
Поэтому ни как часть литерала, ни наоборот - в качестве контейнера для нескольких литералов, плейсхолдер выступать не может. Всегда должно соблюдаться жесткое правило: 1 плейсхолдер - 1 литерал.

Зачем работать с чистым SQL, ведь ORM лучше!
ORM - не лучше.
Это параллельные вещи.
Наличие класса для корректной и безопасной работы с БД никак не мешает использованию ORM-а.
Во-первых, сами методы ORMа, которые работают с БД, можно написать с помощью этого класса.
Во-вторых, средств ORM-а не всегда хватает для работы со сложными запросами. И тут снова пригодится класс, который не оставит разработчика безоружным против инъекций.