Защищают ли подготовленные выражения/переменные полностью от SQL инъекций?
@phpproglib
Что такое sql-инъекции и как они происходят?
$pdo->query("select * from users where login = '" . $login ."'
and pass_md5 = '" . md5($pass) . "'");
Что здесь видит разработчик? Подстановку данных в строку. С точки зрения PHP здесь ничего опасного нет. С точки зрения неопытного PHP-разработчика, проверяющего свой код - код работает, ему ведь не пришло в голову написать что-то странное в логин.
Приключения начинаются, когда кто-то вместо логина вводит admin' or '1'='1. Здесь необходимо напомнить, что SQL - штука изначально текстовая. Что после конкатенации на PHP получит СУБД?
select * from users where login = 'admin' or '1'='1'
and pass_md5 = 'какой-тоmd5'
Это одна строка непрерывного текста. Как СУБД должна понять, что этот запрос отличается от задуманного? Это запрос, он синтаксически корректен, его можно выполнить - СУБД его и выполняет. Но запрос уже делает не то, что хотел сказать разработчик.
Опасность и распространённость SQL-инъекции именно в текстовой сущности запроса. Очень просто подставить в нужное место переменную с данными - но это путь к ошибке и так делать нельзя.
Защищают ли подготовленные выражения от sql-инъекций?
Если говорить о числовых и строковых литералах в запросе - то да, защищают.
Этого достаточно и ничего больше не надо делать?
В общем случае - ничего. В случае с ПДО также желательно еще выставлять кодировку соединения в DSN, но это в любом случае нужно делать.
Идея подготовленных выражений не в том, чтобы отправить данные и запрос отдельно, а в том, чтобы добавлением данных в запрос занимался не программист, а драйвер БД. А уж как оно у него там внутри реализовано - дело десятое.
Примечание
Должен добавить, что существует теоретическая возможность так специально настроить свою систему, чтобы она пропускала инъекции в режиме эмуляции. Для этого потребуется две вещи:
- специальным образом настроить mysql, задав режим NO_BACKSLASH_ESCAPES
- использовать двойные кавычки вместо одинарных в качестве ограничителей строк
Это был баг в mysql, который был исправлен только недавно. Подробнее можно почитать в этом посте на SO.
Ограничения
Куда интереснее здесь вопрос, что делать, когда подготовленные выражения использовать невозможно. Как говорилось выше, параметры в запросе можно использовать дла замены только строковых или числовых литералов. Но бывают случаи, когда в запрос надо подставить не данные, а имя столбца. Вот пример такой ситуации с разбором неправильных решений (по-английски): An SQL injection against which prepared statements won't help.
Ситуация нечастая, но о ней надо знать и быть к ней готовым.
Удобство
В конце концов, подготовленные выражения просто удобнее. Сравним олд скул
$name = $mysqli->real_escape_sring($_GET['name']); $price = $mysqli->real_escape_sring($_GET['price']); $color = $mysqli->real_escape_sring($_GET['color']); $sql = "SELECT * FROM goods WHERE name='$name' and color='$color' and price > '$price'"; $res = $mysqli->query($sql);
и PDO prepared statements
$stmt = $pdo->prepare("SELECT * FROM goods WHERE name = ? and color = ? and price > ?");
$stmt->execute([$_GET['name'],$_GET['price'],$_GET['color']]);
-- компактно, аккуратно и безопасно.
Мифы про экранирование
Отдельное замечание по поводу "конструкций вида mysqli_real_escape_string". В том-то и штука, что в отличие от подготовленных выражений, эти конструкции никакого отношения к защите от SQL инъекций не имеют. Эта конструкция выполняет строго определенную и очень специализированную синтаксическую функцию. применение же её "для защиты от инъекций" гарантированно к такой инъекции и приведет.
Пример такого нецелевого использования приведен по ссылке выше, но можно привести и другой, совсем уж дурацкий но от этого еще более наглядный:
$id = $mysqli->real_escape_string($_GET['id']); $sql = "SELECT * FROM table WHERE id=$id";
Если думать, что функция служит "для защиты от инъекций", то этот код логичен. Однако в реальности он дает возможность приписать дальше практически любой запрос через UNION и получить классическую инъекцию.
При этом не надо ударяться и в другую крайность - примененная по назначению, для экранирования спецсимволов в строках, mysqli_real_escape_string прекрасно справляется с инъекциями, просто в качестве побочного эффекта.