Пользователь
0,0
рейтинг
6 августа 2012 в 09:22

Разработка → Защита от SQL-инъекций в PHP и MySQL recovery mode

К своему удивлению, я не нашёл на Хабре исчерпывающей статьи на тему защиты от инъекций. Поэтому решил написать свою.

Несколько пространный дисклеймер, не имеющий прямого отношения к вопросу
Давайте признаем факт: количество статей (и комментариев) на тему защиты от SQL-инъекций, появившихся на Хабре в последнее время, говорит нам о том, что поляна далеко не так хорошо истоптана, как полагают некоторые. Причём повторение одних и тех же ошибок наводит на мысль, что некоторые заблуждения слишком устойчивы, и требуется не просто перечисление стандартных техник, а подробное объяснение — как они работают и в каких случаях должны применяться (а в каких — нет).

Статья получилась довольно длинной — в ней собраны результаты исследований за несколько лет — но самую важную информацию я постараюсь компактно изложить в самом начале, а более подробные рассуждения и иллюстрации, а так же различные курьёзы и любопытные факты привести в конце. Также я постараюсь окончательно развеять множественные заблуждения и суеверия, связанные с темой защиты от инъекций.

Я не буду пытаться изображать полиглота и писать рекомендации для всех БД и языков разом. Достаточное количество опыта у меня есть только в веб-разработке, на связке PHP/MySQL. Поэтому все практические примеры и рекомендации будут даваться для этих технологий. Тем не менее, изложенные ниже теоретические принципы применимы, разумеется, для любых других языков и СУБД.

Сразу отвечу на стандартное замечание про ORM, Active record и прочие query builders: во-первых, все эти прекрасные инструменты рождаются не по мановению волшебной палочки из пены морской, а пишутся программистами, используя всё тот же грешный SQL. Во-вторых, будем реалистами: перечисленные технологии — хорошо, но на практике сырой SQL постоянно встречается нам в работе — будь то legacy code или развесистый JOIN, который транслировать в ORM — себе дороже. Так что не будем прятать голову в песок и делать вид, что проблемы нет.

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

Ещё только начав интересоваться темой защиты от инъекций, я всегда хотел сформулировать набор правил, который был бы одновременно исчерпывающим и компактным. Со временем мне это удалось:

Правила, соблюдение которых гарантирует нас от инъекций


  1. данные подставляем в запрос только через плейсхолдеры
  2. идентификаторы и ключевые слова подставляем только из белого списка, прописанного в нашем коде.

Всего два пункта.
Разумеется, практическая реализация этих правил нуждается в более подробном освещении.
Но у этого списка есть большое достоинство — он точный и исчерпывающий. В отличие от укоренившихся в массовом сознании правил «прогонять пользовательский ввод через mysql_real_escape_string» или «всегда использовать подготовленные выражения», мой набор правил не является катастрофическим заблуждением (как первое) или неполным (как второе).

Но вперёд, читатель — перейдём уже к подробному разбору.

Плейсхолдеры — подстановка данных

В принципе, тут всё просто: любые данные должны попадать в запрос не напрямую, а через некоего представителя, подстановочное выражение.
Запрос пишется в таком, например, виде,
SELECT * FROM table WHERE id > ? LIMIT ?
а данные добавляются и обрабатываются отдельно.
Но чем это лучше «обычного эскейпинга»? А всем:
  • Во-первых, «эскейпинг» вообще не имеет никакого отношения к безопасности. Представьте себе!
  • Во-вторых, код становится банально короче. Никаких тебе mysql_real_escape_string(), ни даже intval() — вся обработка скрыта внутри.
  • В-третьих, код становится проще. Не нужно запоминать различные правила для форматирования разных частей запроса
  • В-четвёртых, использование плейсхолдеров (при условии, что они корректно обрабатываются) гарантирует нас от инъекций через данные. «Это же очевидно!» — скажете вы — «стоило ли городить отдельный пункт?» Стоило. Тот же эскейпинг защиту не гарантирует. Плюс сокращение правил форматирования до одного — перепутать что-либо тут будет затруднительно
  • В-пятых — и в самых главных — мы обрабатываем данные ровно там, где нужно! Это очень важный момент, который многие не понимают. В классических «похапешных» учебниках форматирование данных для SQL разбросано по всему коду. А в старых версиях PHP оно и вовсе начиналось ещё даже до начала выполнения кода — что совсем уже ни в какие ворота не лезет! Такая ситуация приводит к тому, что одни данные форматируются дважды, другие — только наполовину, а третьи — и вовсе ни разу или совсем не так, как нужно, без малейшей пользы. При этом отформатированные для SQL данные вдруг оказываются в HTML или куках, что тоже не прибавляет радости пользователям и разработчикам.

Поэтому самым лучшим вариантом будет форматировать данные непосредственно перед исполнением запроса — таким образом мы всегда будем уверены в том, что данные форматируются корректно, это делается только один раз, и отформатированные данные попадут строго по назначению — в БД и никуда больше.
И вот как раз целям такой — своевременной, безопасной и корректной — обработки данных и служат плейсхолдеры, давая нам гарантию безопасности, в то же время упрощая код.

Примеры использования:

Из мануала:
$stmt = $dbh->prepare("SELECT * FROM REGISTRY where name LIKE ?");
$stmt->execute(array("%$_GET[name]%"));
$data = $stmt->fetchAll();

Пример того, к чему стоит стремиться:
$ban = $db->getRow("SELECT 1 FROM ban WHERE ip = inet_aton(s:)", $ip);

Как видите, совсем несложно, а так же — при умелом использовании — выходит гораздо короче, чем составлять запрос руками. Вы всё ещё хотите писать по-старинке?

Важное замечание: Разумеется, подстановка данных через плейсхолдеры должна производиться всегда, вне зависимости от источника данных или каких бы то ни было других условий.

Идентификаторы и ключевые слова — белые списки

Подавляющее большинство статей, посвящённых инъекциям, совершенно упускают этот момент из виду. Но реальность такова, что в ней мы сталкиваемся с необходимостью подставлять в запрос не только данные, но и другие элементы — идентификаторы (имена полей и таблиц) и даже элементы синтаксиса, ключевые слова. Пусть даже такие незначительные, как DESC или AND, но требования к безопасности таких подстановок всё равно должны быть не менее строгими!

Разберем довольно банальный случай.
У нас есть база товаров, которая выводится пользователю в виде HTML таблицы. Пользователь может сортировать эту таблицу по одному из полей, в любом направлении.
То есть, как минимум, со стороны пользователя к нам приходит имя колонки и направление сортировки.
Подставлять их в запрос напрямую — гарантированная инъекция. Привычные методы форматирования здесь не помогут. Подготовленные выражения ни с идентификаторами, ни с ключевыми словами не приведут ни к чему, кроме сообщения об ошибке.
Единственное решение — белый список.
Это, разумеется, не бином ньютона, и многие разработчики легко по ходу дела реализуют эту парадигму на ходу, впервые столкнувшись с необходимостью подстановки имени поля в запрос. Тем не менее, статья о защите от инъекций без этого правила будет неполной, а сама защита — дырявой.

Суть метода заключается в том, что все возможные варианты выбора должны быть жёстко прописаны в нашем коде, и в запрос должны попадать только они, на основании пользовательского ввода.

Пример применения:

$order   = isset($_GET['order']) ? $_GET['order'] : ''; // просто для полноты кода
$sort    = isset($_GET['sort'])  ? $_GET['sort']  : '';

$allowed = array("name", "price", "qty"); //перечисляем варианты
$key     = array_search($sort,$allowed); // ищем среди них переданный параметр
$orderby = $allowed[$key]; //выбираем найденный (или, за счёт приведения типов - первый) элемент. 
$order   = ($order == 'DESC') ? 'DESC' : 'ASC'; // определяем направление сортировки
$query   = "SELECT * FROM `table` ORDER BY $orderby $order"; //запрос 100% безопасен

Раньше я предполагал, что для идентификаторов достаточно плейсхолдера. Но со временем пришло понимание недостатков этого метода:
  • во-первых, в случае неверного имени поля запрос вызовет ошибку. А ошибки — это всегда плохо
  • во-вторых, и гораздо более важных: если подставлять имена полей без предварительной фильтрации, автоматом, только эскейпя их, можно получить инъекцию другого рода — ведь пользователь тогда может вписать в те имена полей, которые ему изменять не положено! Скажем, если мы формируем SQL запрос автоматически на базе массива $_POST (при этом правильно форматируя имена полей!), то хакер при регистрации добавляет в форму поле admin со значением «1» и становится админом

Так что теперь я использую оба метода:
Сначала получаю идентификатор из белого списка.
А потом добавляю его через плейсхолдер — просто чтобы не заниматься форматированием вручную. И для единообразия. В этом случае последняя строчка будет выглядеть, как
$query = "SELECT * FROM `table` ORDER BY n: $order";

В принципе, этой информации достаточно, чтобы начать писать полностью безопасные запросы. Но, как водится, в реальной жизни случаются разные нюансы, да и разобраться с механизмом плейсхолдеров хотелось бы поподробнее.

Работа с плейсхолдерами


Для начала нужно понимать, что существует два варианта реализации плейсхолдеров — серверный и клиентский:
  • В первом случае запрос так и уходит на сервер с плейсхолдерами, а данные отправляются отдельно от него. По-английски называется native prepared statements — «родные» подготовленные выражения — то есть, обработка плейсхолдеров осуществляется самой СУБД, на сервере. Для краткости я буду использовать наименование «серверные плейсхолдеры».
  • Во втором случае данные форматируются и подставляются в строку запроса на место плейсхолдеров прямо на клиенте, формируя классический SQL запрос, который затем уходит в базу обычным порядком.
Каждый из способов имеет свои достоинства и недостатки, которые мы рассмотрим ниже.
Следует также помнить, что PDO сидит на этих двух стульях одновременно — по умолчанию работает по второму варианту, лишь эмулируя первый. Эту функциональность можно отключить, заставив PDO отправлять на сервер данные отдельно от запроса.
$dbh->setAttribute( PDO::ATTR_EMULATE_PREPARES, false );
Но поскольку даже эмуляция делается без участия программиста, ниже мы будем рассматривать PDO как представителя серверных плейсхолдеров.

Серверные плейсхолдеры

Для начала давайте сформулируем — почему вообще возможны инъекции?
По факту, SQL запрос представляет собой программу. Полноценную программу — с операторами, переменными и строковыми литералами. Проблема же состоит в том, что мы эту программу собираем динамически, на ходу. В отличие от наших PHP скриптов, которые написаны раз и навсегда, и не изменяются на основе поступающих данных, SQL запрос каждый раз динамически формируется заново. И, как следствие, неверно отформатированные данные могут испортить запрос, или даже поменять его, подставив непредусмотренные нами операторы. Собственно, именно в этом и состоит суть инъекций.

Что же нам предлагает серверная обработка плейсхолдеров?
Очень простую вещь: мы вносим в нашу программу такое понятие, как… переменные! Да-да, плейсхолдер — это обычная переменная, которая жёстко прописана в нашем SQL-«скрипте» и не меняется в зависимости от данных. А сами данные едут на сервер отдельно от запроса, и никогда с ним не пересекаются. Только после того, как запрос будет интерпретирован, данные будет использованы уже непосредственно на этапе исполнения.
На практике это выглядит так: при вызове prepare() наш запрос едет на сервер прямо в таком виде — с плейсхолдерами/переменными, сервер его парсит и сигнализирует — «всё окей, готов принимать данные» (ну, или сообщает об ошибке). А затем, при выполнении execute(), на сервер едут уже данные (причём не в текстовом виде, а в бинарном пакете, по структуре напоминающем тот, в котором возвращается результат запроса), и участвуют уже непосредственно в выполнении.

В теории звучит очень заманчиво.
Однако на практике, к сожалению, в имеющихся библиотеках для работы с Mysql в PHP реализация работы с подготовленными выражениями ещё очень далека от идеала.
Достаточно привести такие, к примеру, факты:
  • Такая банальная возможность, как получение строки результата в массив (аналог mysql_fetch_array()), для запросов, использующих подготовленные выражения, была добавлена в mysqli только в версии в 5.3(!). До этой версии библиотека была практически неюзабельна без адовых извращений.
  • То же самое касается задания кодировки соединения в PDO — это стало возможным только в той же 5.3. Во всех предыдущих версиях задать кодировку соединения в PDO было попросту невозможно (что — справедливости ради следует признать — не является большой проблемой при выключенном режиме совместимости, но — дыра-с!).
  • mysqli в некоторых случаях пытается зарезервировать столько памяти, сколько максимально вмещает поле в БД — осторожнее с mediumtext-ами!
Эти факты говорят нам о том, что обе библиотеки до сих пор довольно сырые, и мы можем ожидать от них отсутствия и другого необходимого функционала.

Перечислим основные недостатки имеющихся библиотек

  • многословность
  • отсутствие некоторых полезных плейсхолдеров
  • невозможность получить классический SQL запрос для целей отладки
  • возможные проблемы с производительностью* (этот вопрос будет рассмотрен ниже)

и разберём их подробнее

Многословность.

Возьмём такую, к примеру, востребованную операцию, как получение всех строк результата запроса в двумерный массив. В mysqli до сих пор нет такой функции. Или, скажем, привязывать переменные к плейсхолдерам там можно только отдельной функцией.
В итоге, на получение данных одного запроса в массив у нас уйдёт минимум девять(!) строк кода:
$data  = array();
$query = "SELECT Name, Population, Continent FROM Country WHERE Continent=? ORDER BY Name LIMIT 1";
$stmt->prepare($query);
$stmt->bind_param("s", $continent);
$stmt->execute();
$result = $stmt->get_result();
while ($row = $result->fetch_array(MYSQLI_NUM)) {
  $data[] = $row;
}

Причём большая часть этого кода не несёт никакой смысловой нагрузки, является абсолютно одинаковой для всех выполняемых запросов и при этом повторяется множество раз в сотнях скриптов.
при том, что для получения данных вполне достаточно всего двух строк:
$query = "SELECT Name, Population, Continent FROM Country WHERE Continent=? ORDER BY Name LIMIT 1";
$data  = $db->getAll($query,$continent);

У PDO с этим немного лучше — там догадались разрешить передавать массив в execute() и есть метод fetchAll(). Но всё равно приходится писать множество ненужного однообразного кода для самых простых операций.
В частности — вернёмся к теме нашей статьи — биндинг. Вот есть у меня переменная $_GET['id']; я хочу привязать её к плейсхолдеру. Отлично, я могу это сделать прямо в execute()… но только сделав её массивом. Зачем? Неужели сам драйвер не может сделать это за меня?

Недостаточность функциионала

Ещё одна проблема, которая уже поднималась в комментариях к недавней статье — оператор IN(). Сделать для него подстановки — задача весьма нетривиальная. Хотя казалось бы, именно для таких случаев плейсхолдеры-то как раз и придуманы:
$conts = array('Europe','Africa','Asia','North America');
$query = "SELECT * FROM Country WHERE Continent IN(?) ORDER BY Name LIMIT 1";
$data  = $db->getAll($query,$conts);

Вместо десятков строк хитровыдуманного кода.
Какой вывод мы можем из этого сделать? Надо допиливать.

Невозможность получить SQL запрос

Кто-то скажет, что ему это не нужно, кто-то изворачивается и пишет запрос руками или использует хитрый софт. Будем уважать мнение этих людей, но факт остаётся фактом — функционал вывода готового запроса удобен для отладки, а серверные плейсхолдеры его не позволяют.

Производительность

Обычно апологеты серверных подготовленных выражений напирают на тот факт, что парсинг запроса делается только один раз.
К сожалению, для веб-приложения это не работает. Та копия скрипта, которая выполнила prepare(), выполняет execute() для этого запроса ровно 1 раз и благополучно умирает. А новая копия заново делает prepare. В итоге выходит больше работы там, где хотели сэкономить.
В комментариях к недавней статье мне указали на ещё один потенциальный выигрыш в скорости — кэширование плана запроса. И ведь действительно, даже если мы делаем prepare для каждого запроса, то БД может закэшировать один и тот же запрос для разных данных. И получить план исполнения без парсинга, а простым сравнением строк!
Увы, я не настолько силён во внутреннем устройстве MySQL, чтобы подтвердить или опревергнуть как наличие самого механизма, так и его практическую эффективность.
При этом, насколько мне известно, под большими реальными нагрузками серверные подготовленные выражения проигрывают по скорости стандартным SQL запросам.
В любом случае, тема открытая и ещё ждёт своего исследователя. В конце концов, версии СУБД растут и результаты тестов устаревают.

В общем, мы выяснили, что средства, которые предоставляет нам СУБД и драйверы для работы с ней, оказались не так хороши, какими представлялись в рекламе. И возникает вопрос — а можем ли мы реализовать работу с плейсходерами самостоятельно? И возникает ответ — можем!

Самостоятельная реализация плейсхолдеров


Вообще, на самом деле никто не мешает нам улучшить юзабилити имеющихся библиотек, не заморочиваясь на плейсхолдеры. Скажем, написать обёртку над PDO, которая реализует недостающий функционал, используя при этом собственные плейсхолдеры PDO (или mysqli, как это сделано в библиотеке, опубликованной год назад на Хабре).

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

Принципы форматирования различных элементов SQL запроса

Реализовать самодельные плейсхолдеры совсем несложно. Примитивный парсер уже встроен в PHP. Единственное, что нам нужно — это научиться отличать различные элементы запроса. Но это очень важный момент, на котором стоит остановиться поподробнее.
Поскольку правила форматирования зависят от типа элемента, нам надо, во-первых, чётко понимать, какой именно элемент запроса мы в него подставляем. А во-вторых, нам надо как-то сообщить эту информацию обработчику плейсходеров.

Для начала давайте определимся, из каких элементов вообще может состоять запрос?
Возьмём, для примера, такой SQL:
INSERT INTO `db`.`table` as `t1` VALUES('string',1,1.5,NOW());
В нём можно выделить три основные группы элементов:
  • собственно элементы языка SQL — операторы, встроенные функции, переменные и пр.
  • идентификаторы (имена баз данных, таблиц и полей)
  • литералы (данные, подставленные непосредственно в запрос) нескольких разных типов.
Нас будут интересовать последние два пункта, поскольку только они требуют специального форматирования.

Теперь подумаем, как сообщить информацию о типе подставляемых данных нашему обработчику. Имеющиеся решения делают это криво (что уже не должно вас удивлять). Варианта тут два: либо тип приходится задавать, вызывая функцию биндинга (что сразу в разы усложняет код), либо не задавать его вовсе, как делает PDO, если передать данные прямиком в execute(). Но совсем без типа обойтись нельзя, и поэтому все переданные в execute данные PDO трактует, как строки. Что приводит к забавным последствиям если PDO работает в режиме совместимости: при попытке передать параметры для LIMIT-а в execute(), PDO вылетит с сообщением об ошибке, в котором можно разглядеть кавычки, которыми честная библиотека обрамила оффсет и лимит!

В общем, нужно другое решение. И оно есть! Но о нём мы поговорим ниже, а пока рассмотрим правила форматирования.

Форматирование идентификаторов

Вообще, правила именования идентификаторов довольно обширные. Но учитывая, что для безопасности мы пользуемся белыми списками, а плейсхолдер — только для форматирования, то достаточно будет этих двух:
  • идентификатор должен быть заключён в обратные одинарные кавычки (backticks)
  • если такая кавычка встречается в имени — она должна быть экранирована удвоением.

function escapeIdent($value)
{
	return "`".str_replace("`","``",$value)."`";
}

Лирическое отступление:
Нужно или не нужно форматировать идентификаторы? Ведь в большинстве случаев это не требуется?
Если запрос пишется руками, то необходимость можно определить на месте: работает запрос — можно не форматировать; вылетает с ошибкой на идентификаторе — надо форматировать.
Если же мы используем плейсхолдер — то есть, добавляем идентификатор в запрос динамически — форматировать надо обязательно, поскольку мы не знаем, какое имя поля будет подставлено в запрос, и, следовательно, требуется ли ему форматирование, или нет. Значит, будем форматировать всё.

Собственно, если говорить об использовании плейсхолдеров в целом, именно последовательное, без исключений, применение правил форматирования и позволяет говорить о гарантированной защите от инъекций и — что немаловажно — ошибок. Ведь форматирование в первую очередь делается для того, чтобы обеспечить синтаксическую корректность запроса. А защита от инъекций является всего лишь побочным эффектом.
Поэтому я предпочитаю употреблять термин «форматирование», а не «эскейпинг»

Форматирование строковых литералов

Самая избитая, казалось бы, тема. Но, как показали обсуждения недавних статей, многие до сих пор путаются если не в понимании, то как минимум — в формулировках. Итак, давайте сформулируем правила форматирования строк в SQL.
  • строка должна быть заключена в кавычки (одинарные или двойные, но поскольку двойные могут быть использованы для идентификаторов, лучше всегда использовать одинарные)
  • в строке должны быть экранированы спецсимволы по списку. Для этого API предоставляет специальную функцию. Для корректной работы этой функции должна быть правильно задана кодировка соединения

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

Вроде бы, простые правила? Но просто поразительно, сколько людей им не следует.
и даже в документации по PHP, в статье про mysql_real_escape_string() написана дичь: «Если не пользоваться этой функцией, то запрос становится уязвимым для взлома с помощью SQL-инъекций. » — как будто если её использовать для чисел или идентификаторов, то это хоть чему-то поможет!

А вот разработчиков PDO можно похвалить — они поступили совершенно логично, выполняя оба правила вместе: функция PDO::quote() делает не половину дела, а всё целиком — эскейпит строку и заключает её в кавычки. Поступим так же и мы:
 function escapeString($value)
{
    return  "'".mysqli_real_escape_string($this->connect,$value)."'";
}

Остаётся только добавить, что «правильно задать кодировку» можно только функциями mysqli_set_charset / mysql_set_charset, а в PDO — в DSN.

Форматирование чисел

Теоретически, в большинстве случаев числа можно форматировать как строки, и тогда задача сведётся к предыдущей. Но есть три проблемы
  • режим STRICT MODE в mysql, который, будучи включённым, в некоторых случаях будет выдавать ошибки при попытке выдать строку за число
  • оператор LIMIT, в котором использование строк не предусмотрено вовсе
  • замечания специалистов по mysql о том, что тип литерала очень важен, и играет роль при планировании и исполнении запроса. Сам я в этом не силён, и буду рад подробному комментарию.
В общем, числа форматировать тоже будем. Тем более, что с помощью плейсхолдеров это не представляет проблемы.
Проблему представляет другое — недостаточноая разрядность встроенных механизмов приведения типов в PHP. Поэтому если вам надо работать с целочисленными значениями, большими чем PHP_INT_MAX, используйте регулярки для валидации. Ну а в обычном случае можно использовать intval().

Числа же с десятичной точкой желательно проверять только регуляркой, поскольку в PHP нет типа данных, аналогичного супер-полезному типу DECIMAL в MySQL.

«Ленивые плейсхолдеры»

Итак, приступим к реализации.
Самый первый вариант, который приходит в голову — это sprintf(). Собственно, подстановочные выражения этого семейства функций и есть настоящие плейсхолдеры. Причём типизованные плейсхолдеры! То есть, то, как именно будут обрабатываться данные, определяет сам плейсхолдер. Это, на самом деле, не такая уж незначительная вещь — ни авторы PDO, ни авторы mysqli так до неё и не додумались. И указать тип подставляемых данных в этих библиотеках можно только вызвав специальную функцию.

В общем, sprintf снимет с нас заботу о любых типах данных кроме строк. Разумеется, sprintf никак не форматирует строки, а выдаёт их как есть. Но нас это не устраивает — у нас есть жёсткие правила форматирования. И это именно правила, а не правило. Как мы помним, эскейпинг — всего лишь пол-дела.
Но поскольку такую ответственную работу, как добавление кавычек, мы не можем отдать на откуп программисту, наш код должен это делать сам. Тупо строковой заменой. Это неидеальное решение, но для учебного кода подойдёт.

Кроме этого мы будем эскейпить все передаваемые в функцию данные. Да, это неправильно. Но вреда от этого не будет, но зато мы сохраним главное преимущество sprintf() — тот факт, что она будет парсить строку за нас.
Почему не будет вреда? Ведь только что я рассказывал, что эскейпить что-либо кроме строк — бесполезно и опасно! Да, если мы больше никак не обрабатываем остальные данные. Но поскольку оставшимися вариантами плейсхолдеров являются только числовые, инъекция через них не пройдёт.
В итоге у нас получилась функция
function query(){
  $query = array_shift($args);
  $query = str_replace("%s","'%s'",$query);
  foreach ($args as $key => $val) {
    $args[$key] = mysql_real_escape_string($val);
  }
  $query = vsprintf($query, $args);
  if (!$query) return FALSE;

  $res = mysql_query($query) or trigger_error("db: ".mysql_error()." in ".$query);
  return $res;
}

которая неплохо справляется со своими обязанностями по подстановке данных в запрос.
причём с защитой от дурака: если мы перепутаем плейсхолдер, и поставим, к примеру, %s вместо %d в операторе LIMIT, то получим ошибку запроса на этапе разработки, но не инъекцию (как это произошло бы в случае эскейпинга).

Разумеется, реализация на основе printf() не лишена недостатков — нaдо следить, чтобы не было лайков, прописанных прямо в запросе, плюс всего три типа плейсхолдеров.
Но для тех, кто ну ооочень не любит ООП, приведу здесь функцию, которую я написал много лет назад и которую можно просто скопировать в конфигурационный файл и начать пользоваться, получив безопасный способ выполнения запросов, ко всему ещё и сокращающий код получения данных в 3-4 раза.
показать код
function dbget() {
  /*
  usage: dbget($mode, $query, $param1, $param2,...);
  $mode - "dimension" of result:
  0 - resource
  1 - scalar
  2 - row
  3 - array of rows
  */
  $args = func_get_args();
  if (count($args) < 2) {
    trigger_error("dbget: too few arguments");
    return false;
  }
  $mode  = array_shift($args);
  $query = array_shift($args);
  $query = str_replace("%s","'%s'",$query); 

  foreach ($args as $key => $val) {
    $args[$key] = mysql_real_escape_string($val);
  }

  $query = vsprintf($query, $args);
  if (!$query) return false;

  $res = mysql_query($query);
  if (!$res) {
    trigger_error("dbget: ".mysql_error()." in ".$query);
    return false;
  }

  if ($mode === 0) return $res;

  if ($mode === 1) {
    if ($row = mysql_fetch_row($res)) return $row[0];
    else return NULL;
  }

  $a = array();
  if ($mode === 2) {
    if ($row = mysql_fetch_assoc($res)) return $row;
  }
  if ($mode === 3) {
    while($row = mysql_fetch_assoc($res)) $a[]=$row;
  }
  return $a;
}
?>


примеры использования:
$name = dbget(1,"SELECT name FROM users WHERE id=%d",$_GET['id']); // если нам нужно только имя
$user = dbget(2,"SELECT * FROM users WHERE id=%d",$_GET['id']); // если нужна вся строка

$sql  = "SELECT * FROM news WHERE title LIKE %s LIMIT %d,%d"; 
$news = dbget(3,$sql,"%$_GET[search]%",$start,$per_page); //если нужен массив

Несмотря на некоторую неуклюжесть, эта функция дорога моему сердцу, поскольку написана в строгом следовании известным принципам: и если KISS ей можно поставить в упрёк, то применение её делает код очень и очень DRY.

Но все мы понимаем, что это тупиковая ветвь. И нам нужен полноценный класс.
Увы, объём статьи уже и превысил все разумные размеры, и создание класса надо выносить в отдельный пост. Остановлюсь здесь лишь на нескольких вопросах.

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

Во-вторых, для нашего класса мы используем поистине блистательную идею типизованных плейсхолдеров. А заодно поживимся у PDO плейсхолдерами именованными.
Пусть наш плейсхолдер имеет вид
[a-z]:[a-z]*

Например,
i:
или
s:name

В первом случае это будет анонимный плейсхолдер, а во втором — именованный.
первая буква задаёт тип, двоеточие отличает плейсхолдер от других элементов строки, а имя — опционально.

В-третьих, долгожданный плейсхолдер для оператора IN() (для строк)
function createIN($data)
{
	if (!is_array($data))
	{
		throw new E_DB_MySQL_parser("Value for a: type placeholder should be array.");
	}
	if (!$data)
	{
		throw new E_DB_MySQL_parser("Empty array for a: type placeholder.");
	}
	$query = $comma = '';
	foreach ($data as $key => $value)
	{
		$query .= $comma.$this->escapeString($value);
		$comma  = ",";
	}
	return $query;
}

и пара замечаний по нему.
Как можно видеть, парсер выбрасывает исключение на пустой массив. Почему?
Потому что mysql ругнётся на пустой IN(), и выполнять запрос всё равно бесполезно.
Где-то я видел остроумный вариант с implode и кавычками по краям, но так делать нельзя: пустая строка — вполне себе честное значение, но искать её имеет смысл только если она явно была передана в массиве.

DmitryKoterov в своей библиотеке dbSimple эту проблему решает довольно красиво — весь блок с оператором IN заключается в фигурные скобки, и, в случае пустого массива, изымается из запроса целиком. Я не уверен в правильности этого решения, но оно, по крайней мере, весьма остроумно.

Update:
Благодаря идее, которую подали zerkms и david_mz, оказалось, что проблема с пустым массивом решается очень просто!
IN(NULL)
не выдаёт ошибку и всегда возвращает FALSE — идеальное представление для пустого массива.
То есть, вместо него надо просто подставлять NULL:
function createIN($data)
{
	if (!is_array($data))
	{
		throw new E_DB_MySQL_parser("Value for a: type placeholder should be array.");
	}
	if (!$data)
	{
		return 'NULL';
	}
	$query = $comma = '';
	foreach ($data as $key => $value)
	{
		$query .= $comma.$this->escapeString($value);
		$comma  = ",";
	}
	return $query;
}

Первое же исключение я решил оставить, исходя из таких соображений:
Теоретически, мы могли бы превратить скаляр в массив и дальше обрабатывать обычным порядком. Но такие скрытые преобразования типов, хоть и привычны для языка, чреваты логическими ошибками.
В итоге, если массив формируется внутри нашего кода, то эта проверка поможет нам отловить все ошибки формирования на этапе разработки. Ну а если массив приходит извне, то тут тем более нужно кидать исключение.
</update>

Cам же я остановился на варианте с предварительной валидацией массива. В этом мне помогает ещё один метод класса — parse(), который парсит строку с плейсхолдерами, подставляет переданные параметры и выдаёт готовый SQL запрос… или его часть(ведь, в отличие от случая с серверными плейсхолдерами, мы легко можем пропарсить произвольный кусок запроса!). И если первое нам пригодится для отладки, то второе — для случаев, подобных нашему:
if (is_array($array) and $array) {
 $sql .= $db->parse(" AND type IN(a:)",$array);
}

Так же этот метод можно применять для составления многоэтажных условных WHERE:
$w     = array();
$where = '';
if (!empty($_GET['type']))      $w[] = $db->parse("type = s:",    $_GET['type']);
if (!empty($_GET['rooms']))     $w[] = $db->parse("rooms IN (a:)",$_GET['rooms']);
if (!empty($_GET['max_price'])) $w[] = $db->parse("price <= i:",  $_GET['max_price']);
if (count($w)) $where = "WHERE ".implode(' AND ',$w);
$data = $db->getArr("SELECT * FROM table $where LIMIT i:,i:",$start,$per_page);

Вышеупомянутую билиотеку DbSimple, кстати, стоит отметить отдельным пунктом, как пример готовой реализации большей части изложенных здесь идей (в частности, трансляция самописных плейсхолдеров в родные, типизованные плейсхолдеры на 100500 типов и многое другое), отдельно отметив тот факт, что написана она была чуть ли не 10 лет назад, но по какой-то причине не получила широкого распространения.

Сеанс разоблачения суеверий


Поговорим немного о заблуждениях.
Я не знаю другой такой темы в веб-разработке, которая обросла бы подобным количеством заблуждений и суеверий. Разве что тема шаблонизации, хехе.
Судя по всему, механизм здесь такой: с азами защиты от инъекций мы знакомимся очень рано, в самом начале. И заученные правила запоминаются в качестве основ, незыблемых истин. И в дальнейшем мы уж к рассмотрению вопроса не возвращаемся. А надо бы.
Плюс толковой информации в интернете как всегда мало, а самому тестировать и разбираться лень. В итоге, наряду с уже существующими суевериями, в интернет вбрасываются (и подхватываются массами!) новые, не менее непрофессиональные. Попробуем разобраться с некоторыми из них

Функции mysql_* уже давно deprecated в PHP

Грустная тема.
История о том, как лень и невежество хоронят прекрасную библиотеку.

Во-первых, не «функции», а расширение целиком.
Во-вторых, не deprecated, а discouraged, причём не давно, а совсем недавно. Тоже не сахар, но надо быть точнее в формулировках.
В-третьих, никаких реальных причин отказываться от этой прекрасной и стабильной библиотеки нету. И единственная причина, по которой её хотят исключить из языка — не нашлось мейнтейнера, который бы хотел ей заниматься. Ну и плюс негативное общественное мнение, которое сложилось только из-за того, что общество само же не научилось правильно с ней обращаться. Однако против лома не попрёшь, и большие красные предупреждения в мануале говорят нам о том, что с расширения mysql надо срочно переезжать.

Но в этой фразе есть еще один нюанс, очень важный. Из разряда вещей, которые не понимают очень многие начинающие разработчики.
Если говорить именно о «функциях mysql_*», то в коде приложения их действительно быть не должно! Равно как и функций mysqli_*, pdo_* или любых других обращений к «голому» API. Прошу отметить — речь идёт о прикладном коде, а не о коде вообще: просто все обращения к функциям API должны быть упакованы в библиотеки, и в коде приложения нужно обращаться уже к библиотечным функциям. А использование всех этих mysql(i)_query(), и mysql(i)_fetch_array() для исполнения конкретных запросов говорит, увы, о непрофессионализме.
Причина этого, в сущности, очень простая: функции API ужасно избыточны. Для того, чтобы получить строку данных из БД, надо написать десяток строк. И это не учитывая таких необходимых вещей, как обработка ошибок, логирование и профайлинг запросов и многого другого. В итоге, если мы пользуемся функциями API прямо в коде, то он получается, с одной стороны — ужасно избыточным, а с другой… недостаточно функциональным!
Авторы PDO попытались сделать свою библиотеку более юзабельной, но, на мой взгляд, недостаточно. И обращения к PDO надо точно так же инкапсулировать в методы собственного класса.
Ещё один аргумент — сейчас приходится менять mysql_* на что-то другое. В единственном файле библиотеки сделать это куда проще, чем в десятках проектов.

Серебряной пули нет

Как видите — есть. Очень простой набор правил, который несложно реализовать и соблюдать.
Надо сказать, что у меня тоже не сразу получилось. Сначала список не был исчерпывающим, потом — компактным.

Эскейпинг

Здесь целый ворох суеверий. Я надеюсь, что после приведенных выше разъяснений вопросов уже не осталось, но, на всякий случай:
  • «проблема с функцией mysql_real_escape_string в том, что ей вообще пользуются» — это ерунда. Единственная проблема функции mysql_real_escape_string() только в том, что очень многие люди не понимают, для чего она нужна
  • «эскейпинг нужен для входящих данных» — разумеется, это не так. эскейпинг нужен для строк
  • «эскейпинг делает „вредные“ символы „безопасными“» — это полная ерунда. Никаких «вредных» символов не бывает. Грубо говоря, эскейпинг всего лишь экранирует ограничители строк, чтобы они, встретившись в тексте, не разбили строку — вот и всё
  • «эскейпинг в принципе имеет какое-то отношение к защите от инъекций» — ну, вы поняли уже? Эскейпинг нужен для форматирования строк. Причём любых строк, а не только потенциально опасных. А защита обеспечивается уже в качестве побочного эффекта
  • «при использовании подготовленных выражений эскейпингом занимается база» — здесь, проблема, скорее в терминологии. С одной стороны, всё верно — серверные плейсхолдеры обрабатываются базой. Но она не эскейпит данные — это просто не нужно, поскольку данные попросту не попадают в SQL запрос. Забавным исключением здесь является PDO, которая по умолчанию работает в режиме совместимости — то есть, не использует серверные плейсхолдеры, а подставляет данные в запрос сама. Но делает PDO это не на сервере. Плюс, как мы уже знаем, слово «эскейпинг» не является синонимом безопасности. Так что PDO занимается не «эскейпингом», а корректным форматированием данных.

Проверка входящих данных на «опасные» слова и символы

Такие как «UNION», одинарная кавычка и пр. Очень смешной способ защиты. Я, когда вижу на каком-нибудь сайте автора, всерьёз предлагающего очередной её вариант, всегда хочу его спросить — а что было бы, если бы если сам сайт пользовался предлагаемой защитой? Наш горе-изобретатель просто не смог бы опубликовать свой пост, как содержащий те самые «опасные» «стоп-слова».

Универсальная функция защиты данных

Тоже очень распространённое заблуждение среди начинающих пхпшников. «Поставим на входе универсальную функцию, которая будет убивать все „вредные символы“ в поступающих данных и таким образом обезопасим себя от любых инъекций!» — думают они.
Но как мы уже выяснили, «вредных» символов не бывает. Есть только служебные символы, которые имеют определённое значение только в определённом контексте. И совершенно безопасны во всех других случаях.
Также мы выяснили, что в первую очередь нам нужна не безопасность, а корректное форматирование данных. А сделать это можно только тогда, когда мы уже знаем — в каком контексте они будут использоваться. На входе в скрипт мы этого ещё, увы, не знаем. Яркий пример такого подхода представляет собой печально известная директива magic_quotes, ныне, к счастью, изъятая из языка. Обрабатывая совсем не те данные, которые нужно, она создавала иллюзию безопасности, защищая только строки (и только попадающие в скрипт извне).
Так что, пожалуйста, не повторяйте этих ошибок — не форматируйте данные для SQL сразу при попадании их в скрипт.

Кодировки

Тема кодировок относится только к ручному форматированию строк. При обработке плейсхолдеров на сервере никакой проблемы не представляет.
Тема тут старая, «это все придумал Шифлетт в восемнадцатом году» и тогда же Альшанецкий показал, что mysql_real_escape_string() тоже не помогает(!)
Вся фишка в том, что вопреки сказанному в документации, mysql_real_escape_string() не «принимает во внимание кодировку соединения», если только её специально не пнуть. По умолчанию же текущая кодировка — latin1, для которой никакие хитрые настройки не требуются, и mysql_real_escape_string() действует не умнее своей туповатой родственницы mysql_escape_string().
При этом в 2006 году средства сообщить функции mysql_real_escape_string() текущую кодировку в расширении mysql просто не было. С тех пор, впрочем, прогресс сильно ушёл вперёд и средство появилось — функция mysql_set_charset(). Которая и должна использоваться для выставления клиентской кодировки вместо запроса SET NAMES.

В любом случае, инъекция возможна только для некоторых экзотический кодировок. Все однобайтные кодировки и UTF-8 безопасны и так (следствием чего является тот факт, что для них годится и пресловутая addslashes()).

Я не поленился в свое время проверить, как это всё работает. Убедился — работает ровно так, как предсказано.
Под спойлером результаты тестов, если не лень смотреть.
MySQL.

mysql> select version();
+---------------------+
| version()           |
+---------------------+
| 5.0.45-community-nt |
+---------------------+
1 row in set (0.00 sec)

mysql> CREATE TABLE users (
    ->     username VARCHAR(32) CHARACTER SET GBK,
    ->     password VARCHAR(32) CHARACTER SET GBK,
    ->     PRIMARY KEY (username)
    -> );
Query OK, 0 rows affected (0.08 sec)

mysql> insert into users SET username='ewrfg', password='wer44';
Query OK, 1 row affected (0.02 sec)

mysql> insert into users SET username='ewrfg2', password='wer443';
Query OK, 1 row affected (0.03 sec)

mysql> insert into users SET username='ewrfg4', password='wer4434';
Query OK, 1 row affected (0.00 sec)


PHP
<pre><?php
echo "PHP version: ".PHP_VERSION."\n";

mysql_connect();
mysql_select_db("test");
mysql_query("SET NAMES GBK");

$_POST['username'] = chr(0xbf).chr(0x27).' OR username = username /*';
$_POST['password'] = 'guess';

$username = addslashes($_POST['username']);
$password = addslashes($_POST['password']);
$sql = "SELECT * FROM  users WHERE  username = '$username' AND password = '$password'";
$result = mysql_query($sql) or trigger_error(mysql_error().$sql);
var_dump($username);
var_dump(mysql_num_rows($result));
var_dump(mysql_client_encoding());

$username = mysql_real_escape_string($_POST['username']);
$password = mysql_real_escape_string($_POST['password']);
$sql = "SELECT * FROM  users WHERE  username = '$username' AND password = '$password'";
$result = mysql_query($sql) or trigger_error(mysql_error().$sql);
var_dump($username);
var_dump(mysql_num_rows($result));
var_dump(mysql_client_encoding());

mysql_set_charset("GBK");
$username = mysql_real_escape_string($_POST['username']);
$password = mysql_real_escape_string($_POST['password']);
$sql = "SELECT * FROM  users WHERE  username = '$username' AND password = '$password'";
$result = mysql_query($sql) or trigger_error(mysql_error().$sql);
var_dump($username);
var_dump(mysql_num_rows($result));
var_dump(mysql_client_encoding());


Результат
PHP version: 5.3.3
string(29) "ї\' OR username = username /*"
int(3)
string(6) "latin1"
string(29) "ї\' OR username = username /*"
int(3)
string(6) "latin1"
string(30) "\ї\' OR username = username /*"
int(0)
string(3) "gbk"


Характерная деталь, о которой я упоминал ранее:
В PDO до недавнего времени вообще нельзя было выставить кодировку соединения. Функции, аналогичной mysql_set_charset() в PDO нету, а в DSN до версии 5.3 был только муляж параметра charset, который ошибок не выдавал, но и никакую кодировку не выставлял.
Ничего, в сущности, особенного, кроме возможности троллить персонажей, рассказывающих о том, что PDO защищает от всего.

«Уязвимость в LIKE»

Уязвимость в LIKE бывает только одна: применение этого оператора не по назначению.
Почему-то всякие составители статей ужасно любят пугать новичков возможностью подставить в выражение метасимвол. При этом никто не пишет, что если LIKE используется, то метасимвол в любом случае должен быть. Иначе в LIKE нет смысла. То есть, правильным средством борьбы с этой уязвимостью будет не экранирование символов % и _, а неиспользование этой функции там, где нас интересует точное совпадение — для проверки пароля, например. Вот и всё.

Заключение


Хоть целью этой статьи и не является реабилитация расширения mysql, тем не менее я бы хотел отметить, что проблема, как это часто бывает, оказывается не в инструменте, а в руках, которые его держат. И если есть понимание целей и механизмов их достижения, то и с «устаревшим» инструментом можно получить результат не хуже.
Поэтому я старался не насадить одну «религию» вместо другой, разрекламировать новый метод на замену старому, но попытался объяснить, какой цели мы хотим достичь, зачем, и какие есть варианты её достижения.
И если это мне хоть чуточку удалось, я буду считать свою задачу выполненной.

Update: класс для удобной работы с mysql и защиты от SQL инъекций, реализующий изложенные в статье принципы.
FanatPHP @FanatPHP
карма
16,5
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Спецпроект

Самое читаемое Разработка

Комментарии (97)

  • +6
    опять?
    • +10
      ИМХО, данный топик достаточно хороший получился, в отличии от других в стиле «применяйте везде mysql_real_escape_string».
      • 0
        Не соглашусь, что топик хороший, статьи надо писать, чтобы каждый в меру своего понимания мог для себя уяснить суть статьи и вынести что-то полезное, а не от гика для гиков, которые итак это знают. Полезность в этом случае сомнительная.
        • 0
          Вы не могли бы привести пример какого-нибудь места, которое может быть неясно?
          Я старался расписать максимально понятно, но мог и не преуспеть. Но всё поправимо, и я буду рад внести правки, делающие статью более понятной.

          Проблема коммуникации гик-новичок действительно существует — я даже отметил её во вступлении.
          Беда в том, что когда новички пишут для новичков — выходит ещё хуже.
          В общем, если можете указать на конкретные неясности в статья — я был бы благодарен
          • 0
            Я вот сейчас занимаюсь серьёзным проектом и решил всерьёз заняться защитой от инъекций прежде, чем выходить в продакшн. Нагуглилось тысяча всего, но ваша статья оказалась самой исчерпывающей и самой полезной. При этом вполне понятной тому, кто пока не «знал этого и так») Так что большое спасибо!
          • 0
            Не гикам хотелось бы готовых решений. Или прямо ткнуть в них носом, т.к. по тексту они не очень понятны. Лучше пример класса для работы с БД, которым, например, вы пользуетесь (следующая статья не вышла еще?) и описать что с ним делать. Это особенно актуально для проектов, где БД используется на элементарном уровне, поэтому нет людей, которые в этом отлично бы шарили, а защита все-таки нужна…
            • +1
              На тот момент ещё не вышла, а вот только сегодня: habrahabr.ru/post/165069/
              Спасибо за этот комментарий, он заставил меня активизировать работу по доведению класса до публичного состояния :)

              Сравнение с популярными движками есть в этой статье — выше приводятся примеры кода, который нам бы понадобился для PDO и Mysqli. А в новой статье есть примеры того, как это делается в моей библиотеке.
          • 0
            А если бы еще привели сравнение с решениями, используемыми в популярных движках или фреймворках, было бы, совсем здорово. Все движки, которые я видел, обрабатывали данные при получении, а в запрос вставляли без особой обработки
    • +12
      Увы, судя по комментариям, так думают, похоже, многие.
      Не читав, при этом, саму статью, и не имея, следовательно, возможности судить — действительно ли это «опять» или же что-то новое.

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

      Вся надежда остаётся на комментарии по существу, в ответ на которые я могу аргументированно переубеждать заблуждающихся.
  • –4
    SQL Injection по сути это проблема недостаточно грамотных или очень ленивых программистов. Правило тут одно — проверять переданные из формы данные на валидность.
    • +7
      К сожалению, вы неправы. В статье как раз подробно разбирается — почему.
    • +3
      Кавычки в имени (d'Artagnian) вполне валидны, или вы будете выдавать юзеру ошибку на имя типа strstr($name, "DROP TABLE students")? (простите, но у вас SQL injection в никнейме, выберите другой) Надо не валидировать, а экранировать, и всё будет ок.
      • +3
        Я думаю, что проблема тут скорее в терминологии, и elve имел в виду то же самое. Особенно, если учесть, что термин «экранировать» тоже не очень удачный.

        Но на самом деле его ошибка совсем в другом:
        Форматирование данных должно определяться не источником, а получателем данных. С точки зрения защиты от инъекций корректно обрабатывать надо не «переданные из формы» данные, а «передаваемые в запрос».
  • –17
    Смысл городить весь этот огород с плейсхолдерами, если он кушает производительность (парсеры, массивы, пересчеты туда-сюда) да еще и код усложняет?

    Потом еще проблемы обучения сотрудников встают, SQL едионообразен и четок в структуре, а тут у каждого свой синтаксис.
    • +10
      Признайтесь, вы написали комментарий, не читая статью? :)
      На самом деле, всё наоборот — код (прикладной) значительно упрощается.

      Правила форматирования SQL кода, увы, совсем не единообразны. К примеру, строковые литералы надо форматировать одним способом, а числовые — совсем другим.
      В то время как плейсхолдеры, как раз, являются именно таким единообразным способом формирования динамических запросов.

      Вот пример кода с плейсхолдерами

      $data = $db->getAll(«SELECT * FROM news WHERE id IN(a:) AND theme=s:»,$ids,$theme);

      Покажите, пожалуйста, как сделать его проще?

      • –10
        Совершенно такой же запрос будет на голом SQL, только в параметрах будут чищенные переменные. При этом он будет кушать меньше ресурсов, т.к. строка просто соберется используя функции языка, а не самописные функции на php для обработки конструкции.
        • +12
          Подождите-подождите.
          Во-первых, что значит «чищеные»? От чего чищеные?
          Во-вторых, не святым же духом они станут «чищеными»? То есть, какой-то код понадобится?

          Вас не затруднит привести полный код составления запроса, вместе с «очисткой» переменных? А то заявление про «усложнение кода» выглядит несколько голословно, уж извините.

          По поводу же ресурсов, я приведу аналогию. При выводе HTML мы используем примерно такой же принцип — «парсеры, массивы, пересчеты туда-сюда». Но, вроде бы, необходимость использования шаблонизаторов никем не оспаривается, а накладные расходы считаются приемлемой платой за удобство и безопасность.

          • –9
            Данные защищенный от возможных иньекций. Числа интвалом, строки аналогично вашему методу — экранирование, ковычки + еще несколько методов для спокойствия души, массивы чисел функцией разбирающей массив/строку по разделителю чистящей числа и собирающей обратно.

            mysql_query(«SELECT * FROM news WHERE id IN (».clean_int_arr($_GET['ids']))." AND theme=".clean_str($_GET['theme']));
            • +3
              Почему то мне сейчас вспомнились времена, когда magic quotes ещё не были ересью DEPRECATED в PHP 5.3.0 и REMOVED в PHP 5.4.0.
            • 0
              А код для clean_int_arr и clean_str?
  • +2
    Вы уж будьте последовательным — объясните, почему прогонять данные через mysql_real_escape_string это катастрофическое заблуждение?
    • +1
      В качестве единственного средства защиты — это так и есть. Подробное объяснение, мне кажется, есть в статье, причём в нескольких местах. Но если коротко, то основная проблема состоит в том, что mysql_real_escape_string — это лишь часть необходимого форматирования, применимая только к строкам, при этом абсолютно бесполезная для любых других элементов запроса. Не говоря уже о том, что сама по себе эта функция к защите от инъекций не имеет никакого отношения. Она служит для форматирования строк.
  • 0
    Откуда информация о том, что PDO::ATTR_EMULATE_PREPARES по-умолчанию — true?
    • 0
      Ну, во-первых, из личного опыта, а во-вторых, исходя из принципа построения всех публичных фреймфорков, о котором говорил Расмус на конфе — максимальная перестраховка :) А режим эмуляции по определению более совместим.
      • 0
        во-первых, из личного опыта

        Какой личный опыт подсказал Вам, что там true по-умолчанию?

        во-вторых,… максимальная перестраховка

        Максимальная перестраховка? Это отдает непониманием происходящих процессов. Сразу вспоминается анекдот про два jmp подряд, на случай, если первый не сработает.

        Я так понял, что Вы не знаете, но предполагаете? Верно?
        • 0
          Ну, я бы не назвал это предположением. У меня есть опыт.
          Возможно, он недостаточный/устаревший, но это именно практический опыт, а не предположение.

          Если у вас есть опровержение, я с удовольствием его выслушаю и исправлю статью.
          • 0
            Как мне убедиться, что Вы, например, не обманываете меня? Как я могу проверить, что там true? Вы же как-то узнали?
            • +4
              Да, прошу прощения, я не написал.
              Путём выполнения вот такого кода

              try {
              $dsn = 'mysql:dbname=test;host=127.0.0.1;charset=GBK';
              $dbh = new PDO($dsn, $user, $password);
              $dbh->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION );
              $sth = $dbh->prepare(«SELECT id FROM users WHERE name LIMIT ?,?»);
              $sth->execute(array(«1»,«2»));
              } catch (PDOException $e) {
              echo $e->getMessage();
              }

              И прочтения сообщения об ошибке, в котором переданные параметры оказались подставлены на место плейсхолдеров.
              • +4
                Вот оно! Спасибо! Вам стоило привести этот пример в статье. Я на Хабре не раз натыкался на голословные утверждения, особенно касательно PDO, поэтому требую пруфов.

                P.S. Вам стоит поработать над подачей материала — очень тяжело дается Ваша статья.
          • +1
            Всяко бывает…

            https://bugs.php.net/bug.php?id=54638
  • 0
    Хорошо было бы составить некий checklist, с которым стоило бы сверять каждую строчку кода. А то вроде бы и знаешь, а память дырявая — вот и пропустишь некоторые проверки.
    • +1
      Так чеклист же и состоит всего из двух пунктов? Плейсхолдеры для данных и белый список для всего остального.
      Вы считаете, что этого недостаточно? А можете привести какую-то конкретную ситуацию, в которой
      данные рекомендации окажутся неприменимыми или двусмысленными?
      На конкретных примерах всегда лучше видно проблему.
      • +1
        Попытаюсь составить, насколько смогу. Там больше двух, точно.
        • +1
          Было бы очень интересно взглянуть.
          Возможно, речь не о динамической сборке запроса?
          Дело в том, что плейсхолдеры, как раз, и служат для сокращения чек-листа для этой операции. «Добавил через плейсхолдер — и забыл». Разве что тип надо указывать. Но для родных препаредов и это не нужно.
          • +10
            0. хватит шишек:
            0.1. велосипедам — нет! используем известные библиотеки\фреймворки, где уже многое исправлено
            0.2. своевременный апгрейд
            0.3. логи и мониторинг подозрительной активности.

            1. входные данные для каждого слоя:
            1.1. слой контроллера (код): фильтрация данных из суперглобалов (всех)
            1.1.1. инициализация данных, мусор может быть вредным (например, те же автоглобалы)
            1.2. слой модели\данных (sql): используем prepared statements или иные placeholders, хотя бы экранизацию(escaping)
            1.3. слой вида (html): используем escaping везде, где требуется вывод данных
            1.4. слои не должны доверять друг другу и полагаться, что не придут вредные данные.
            1.5. минимизация функциональности: делать что нужно и не больше (например, для отдачи файла НЕ надо его исполнять).

            2. фильтрация данных:(xss+code\sql injection)
            2.1. до максимально узкого значения. например, если должно быть число, пытаемся кастить в число.
            2.2. до более широкого диапазона можно прогонять через регулярки. не забываем про длину.
            2.3. если данные исчислимы (выбор из списка), то сопоставить реальным данным числовой идентификатор.
            2.4. не перестараться, не портить данные «универсальными» чистками.

            3. аутентичность:
            3.1. минимальный срок жизни данных:
            3.1.1. используем сессионные данные, для доступа используем трудноугадываемый идентификатор
            3.1.2. для подтверждений: длинный ОДНОРАЗОВЫЙ ИД.
            3.1.3. логин, повышение прав: регенерация ИД, подтверждение пароля (session hijacking)
            3.1.4. формы и иные stateful запросы также имеют одноразовый ИД (csrf,request\form spoofing)
            3.2. в зависимости от важности данных устанавливаем сложность проверки юзверя (двухфазный вход, генераторы паролей, заставляем сложные пароли)
            3.3. права доступа (как ФС, так и системные)

            4. client is evil:
            4.1. ничего не храним на клиенте, даже в зашифрованном виде. максимум — session id в куках
            4.2. валидация на клиенте бессмысленна, только для удобства пользователя
            4.3. не полагаемся на данные об ОС\браузере\…
            4.4. шифрование на клиенте бессмысленно, но необходимо (если вдруг что-то увели)
            4.5. имена файлов, другие пароли и явки — ложь.

            5. DoS предупреждение:
            5.1. слишком длинные данные + размер загружаемых данных (картинки, например)
            5.2. много неудачных попыток входа
            5.3. много загрузок
            5.4. много запросов, добавляющие данные (напр.комментарии)

            6. О шифровании:
            6.1. все пароли (в базе) — в виде хешей с солью, проверить на криптостойкость и коллизии.
            6.2. для чувствительных данных — шифрование потока по HTTPS\SSL c коротким expiration
            6.3. не храним данные в файлах, особенно — в открытом виде.
            6.4. статика — отдельно от динамики, с соотв. правами доступа (либо читать, либо выполнять, либо писать)
            6.4.1. желательно — статика очень отдельно от динамики (др. сервер, папка, за пределы корня) т.е. чтобы не пересекались

            7. Лишнее:
            7.1. ничего лишнего — например, никаких .svn, паролей, чеклистов, туду и вообще не жизненно важного для работы.
            7.2. никаких инфо-заголовков — об используемом софте, версиях, путях, файлах, именах
            7.3. никаких сообщений об ошибках, кроме подсказок юзверю, что он делает не так и что ему делать так.
            7.4. источник должен быть виден, исключить возможность подделок, пример:
            7.4.1. комментарии должны выглядеть как комментарии и не иначе
            7.4.2. в заголовке всех окон ввода — четко имя сайта и функция, должно быть узнаваемо.
            7.5. лучше использовать pretty urls — и для удобства, и сокрытия данных.
            • +3
              ну это сильно шире инъекций
              • +2
                однако нельзя рассматривать безопасность как-то отдельно по типам.
                «безопасность системы определяется наиболее уязвимым местом» — не знаю, откуда цитата
                • +1
                  Просто в контексте данной публикации речь идет именно о БД, хотя в общем контексте вы правы
                  • +4
                    Мне всегда не хватало такого конспекта. Как думаете, может, стоит опубликовать это отдельным постом?
                    • +1
                      Мне кажется — да, но рекомендовать не возьмусь. Уж очень непредсказуема реакция хабрасаообщества.
                      Кстати, хороший вариант — посмотреть сначала на оценки комментария!

                      В любом случае, я бы сначала поправил п 1.2 — «хотя бы escaping» — это решето.
                      • 0
                        по п.1.2: не всегда удается хорошо отфильтровать данные, особенно если их «непредсказуемость» высока — например, те же комментарии — можно фильтровать по [a-zA-Z0-9_+ ] — но это убьёт юникод потенциально «хорошие» символы.
                        Да и внутри всяческих базадрайверов чаще используется именно экранирование «плохих» символов, так что дырявость решета зависит только от реализации, а это к п.0.1.
                        А вообще 1.2. «хотя бы escaping» относилось именно ко всяким велосипедам и иже, где реализация «prepared statements или иные placeholders» отсутствует или неприменима, т.е. следует понимать так:
                        … а если все совсем плохо, то все равно ни в коем случае не оставлять данные «сырыми», минимум экранировать.
                        • 0
                          > не всегда удается хорошо отфильтровать данные, особенно если их «непредсказуемость» высока

                          Фильтрация к безопасности никакого отношения не имеет. Чтобы гарантированно безопасно вставить данные в базы — достаточно следовать правилу 1 из статьи (плейсхолдеры)
                          • 0
                            В базы — да. В общем случае — вариантов больше.
                            Вообще — давайте остановимся на этом, признав, что говорим о разных вещах, придерживаясь одного мнения :)
                            • 0
                              Ну не совсем одного мнения. Фильтрация — вопрос, касающийся предметной области приложения, а не особенностей работы с тем или иным хранилищем.
              • 0
                А вообще меня очень интересует — что можно еще в этот список добавить? Что я забыл? Какие моменты остались в нем непонятны?
                Список достаточно сумбурен — писал из головы, что сразу вспомнил. Может, его для более удобоваримого чтения как-то можно реорганизовать?
  • –10
    прочитание столь долгого и нудного топика утомило меня…
  • 0
    Остаётся только добавить, что «правильно задать кодировку» можно только функциями...

    А разве так не будет работать?

    $db->query('set names utf8');
    
    • +2
      Нет, в контексте эскейпинга строк — не будет.
      Я там как раз чуть ниже привёл под спойлером код, который это наглядно демонстрирует.

      Запрос SET NAMES влияет только на сервер, а «правильные» способы — и на сервер, и на клиент.
      • 0
        Запустил Ваш код. В первых двух случаях ругается You have an error in your SQL syntax и mysql_query возвращает false. В последнем случае не ругается, возвращает пустой результат.

        PHP version: 5.3.10-1ubuntu3.2
        mysql: 5.5.24-0ubuntu0.12.04.1
  • +4
    «Функции mysql_* уже давно deprecated в PHP» — моя формулировка, даже стыдно немного. У вас получилась отличная статья. Ни одной глупости не увидел, что в наше время редкость для статей по этой теме. Пишите еще.
    • –2
      Вообще-то они действительно deprecated.
      • +2
        Во-первых, это только предложение из мейл-листа. В том же листе и про выход 6.0 писали.
        Во-вторых, там как раз написано, что «Not adding E_DEPRECATED errors in 5.4». Текущая версия сейчас — 5.4. То есть, о таком статусе в настоящем времени говорить можно вряд ли.
        В-третьих, самая простая проверка показывает нам разницу:
        www.php.net/manual/en/function.mysql-list-dbs.php — стоит плашка «Deprecated» (ниже на странице).
        www.php.net/manual/en/function.mysql-query.php — стоит плашка «Use of this extension is discouraged», но про Deprecated ничего нет.

        • 0
          В то же время: www.php.net/manual/en/mysqlinfo.api.choosing.php — «Long term deprecation announced».
          • 0
            А вот собственно и ответ. Т.е. сами разработчики хоронят прекрасную библиотеку.
          • 0
            С этим я согласен, но это никак и не противоречит сказанному мной.
            Deprecated — это совершенно конкретный статус, который выдает конкретную ошибку при соответствующем уровне отображения.
            Discouraged же — некий неформальный термин, который появился в документации по этому расширению буквально на днях.

            Но вообще, я имел в виду не терминологию, а скорее тот факт, что расширение mysql на настолько плохо, как его изображают. И основная проблема не в нем самом, а в непонимании и неумении пользоваться. Во всяком случае, с безопасностью при корректном использовании проблем никаких нет.

            С тем, что сами же разработчики и хоронят, я тоже не спорю — именно это я и написал. Собственно, мне они же сами и ответили в том смысле, что просто никто не хочет заниматься поддержкой этого расширения. Ну и по второй ссылке между строк читается «и так есть два расширения, зачем ещё с со старым возиться?». Что характерно, никакой другой причины там не написано. «Старое» — и всё. Как будто curl — новое.
            • 0
              а какой, кроме обратной совместимости, смысл в mysql?
              есть pdo… есть mysqli…
              в том же mysql как-то не замечено prepare и placeholder'ов…

              давайте еще кучу «проверенного» функционала из 3 и 4 веток потащим?
              • +1
                Я понимаю, что по комментариям читать проще, но я же обо всём этом написал :)

                mysqli — однозначно сырая библиотека. В этом смысле я предпочту старую, но проверенную.
                PDO — это лишний уровень абстракции, над которым всё равно надо городить свою — получается две.

                Функционал prepare достаточно спорный.
                Набор имеющихся placeholder-ов явно недостаточен — то есть, всё равно надо допиливать. Следовательно, парсер всё равно писать — а значит и основные плейсхолдеры реализовать не проблема, даже в mysql ext.

                Если функционал проверенный, то давайте, конечно, притащим. Не вижу никакой проблемы в проверенном функционале, если честно. Что в нём плохого?
                Плохой функционал, типа magic_quotes, тащить, разумеется, не надо. Но отказываться от хорошего только по той причине, что он был в 3 и 4 версии — это как-то странно.

                В принипе, я не защищаю mysql. Но нахожу аргументы запретителей весьма спорными.
                • 0
                  PDO удобно тем, что для mysql и sqlite пишешь одно и то же. Вы для sqlite что используете?
                  • 0
                    Я не использую sqlite.
                • 0
                  А что в mysqli конкретно сырого?
                  • 0
                    Я привёл в статье два примера ошибок, исправленных совсем недавно.
                    Это означает, во-первых, что столь серьёзные баги всплывают до сих пор, а во-вторых — они мешали массовому использованию (а значит — тестированию) экстеншена. Что означает довольно большую вероятность появления новых багов в будущем.
            • 0
              Да, но curl разработчики не собираются удалить его из PHP one day, в отличие от mysql.
              • 0
                Об этом и речь.
                При этом из аргументов у них только «старая» и «две другие есть».
                Ну так для замены «старого» курла даже больше новомодных кунштюков есть — от fopen-wrappers до php_http. Но курл никто из языка не выпиливает.

                Так что если у вас есть ссылка на вменяемое объяснение причин удаления — я был бы благодарен на самом деле. Поскольку, как я уже говорил, я не сторонник этого расширения как такового — я сторонник осмысленных и аргументированных действий вообще.
  • –2
    Зашел сюда увидеть картинку из xkcd.com/327/ и был очень удивлен.
  • +3
    Идея типизированных плейсхолдеров — рулез! Сделать у себя, что ли?:) И за ссылку на библиотечку Котерова спасибо, не видел.

    Но, кстати, я там не нашёл обработки пустого IN() (если только не проверять руками пустоту массива и не передавать DBSIMPLE_SKIP). В принципе, это можно попробовать автоматизировать но есть вероятность, что пустые массивы — не всегда ошибка…
    • 0
      Спасибо за поддержку, Давид!
      А ты правда раньше про dbSimple не слышал? Я-то знал давно, но сначала не понимал всей глубины заложенных в неё принципов, а потом уже стал сам себе учёный и начал своё писать :)
      Вообще, конечно, «не понимал всей глубины» — это беда, и далеко не только моя. И я наивно надеялся на свой талант популяризатора. Но, судя по имеющемуся фидбеку, что-то мне подсказывает, что mission failed…

      Долго чесал репу, откуда я взял про обработку пустого массива в IN
      оказалось — из англоязычной версии: en.dklab.ru/lib/DbSimple/#list6
      Сейчас посмотрел по коду — так и есть, Котеров поступил совершенно логично — пустой массив тоже является триггером для пропуска плейсхолдера:
      case 'a':
          if (!$value) $this->_placeholderNoValueFound = true;
      
      • 0
        Не знал, да. Котеров вообще велик, но очень уж герметичен, я на многое у него натыкаюсь только по случаю.

        А вот что делать, например, если плейсхолдер типа int, а к нему пришла строка «hello, world»?
        • 0
          Если эта строка прошла валидацию согласно правилам предметной области (бизнес логики) приложения — тогда просто приводить к целому (0) и не париться.
        • 0
          Вот буквально в комментариях к предыдущей статье по теме я утверждал, что библиотека для сборки запросов не должна заниматься валидацией данных. Но что-то сейчас гложут меня сомнения.

          И если, скажем, кастить ли пустую строку в 0 — это ещё вопрос, то hello world, пожалуй (или, точнее, несовпадение с "-?\d+") — это повод для исключения. Надо посмотреть по коду, но, вроде бы, столь экстремальное приведение типов осознанно нигде не используется.
          • +1
            Исключение должно было быть брошено валидатором. Если валидатор сказал «окей» — то DAL должен безропотно взять данные и отсанитайзить, совсем не глядя внутрь.
      • +2
        > совершенно логично — пустой массив тоже является триггером для пропуска плейсхолдера:

        Смотря с какой стороны посмотреть. Для меня это абсолютно нелогично.

        Если array(1) генерирует в итоге IN(1), а array(1,2) — IN(1, 2), тогда совершенно логично, что результатом подстановки array() всегда будет пустое множество, т.е. FALSE.
        • 0
          круто, хехе!
          И ведь работает однако! :)
          Прям сегодня внесу изменения в либу :)
        • 0
          В смысле
          IN (FALSE)
          работает, как FALSE. То есть, можно обойтись без громоздких проверок, и даже без котеровского 1=0
          • 0
            Есть мнение, что FALSE заматчится с id = 0, к примеру. Ну или пустой строкой.
            • +2
              false, естественно, заматчится с false.

              Запрос для размышления: select null in(null), false in(false)
              • 0
                Как это связано с моими словами?

                Запрос вам для размышления:

                SELECT FALSE IN (FALSE), 0 IN (FALSE), '' IN (FALSE)

                Поясню свои слова (судя по всему то, что я сказал не для всех оказалось очевидным): пустой массив должен всегда отображаться на пустое множество, для которого единственно верного для всех случаев решения с использованием оператора IN() не существует (см. данный вам выше запрос для размылшения).

                В то время как предикат `AND FALSE` будет работать как ожидается и всегда.
              • 0
                PS: переосмыслил — да, IN(NULL) таки панацея, поторопился
                • +1
                  id IN(NULL) => NULL => false
                  id NOT IN (NULL) => NULL => false;

                  Возможна ошибка.
          • 0
            Как правильно поправил david_mz
            IN (NULL)
            
            конечно же
  • 0
    Добавьте к названию статьи что-то типа "… полноценный плейсхолдер".
  • 0
    вот еще ссылочки на анализаторы инъекций не помешали. Ну а если статья претендует на звание исчерпывающей, ей лучше быть в какой-нибудь вики, чтобы народ дочерпывал то, что автор забыл зачерпнуть
    • +3
      Тот самый редкий случай, когда «народ» лучше не допускать до редактирования текста :-)
  • +2
    Хорошая статья.

    Единственное можно дополнить: у MySQL не только числовые и строковые типы, но есть ещё NULL. Как со строками с NULL работать нельзя, потому что:

    mysql> select isnull(null), isnull('null');
    +--------------+----------------+
    | isnull(null) | isnull('null') |
    +--------------+----------------+
    |            1 |              0 |
    +--------------+----------------+
    1 row in set (0.00 sec)
    


    С датами в MySQL, в принципе, можно работать как со строками. Однако есть нюансы, которые выходят за рамки данной темы.

    А если писать плэйсхолдеры для баз, поддерживающих больше типов плюс пользовательские, всё будет ещё интереснее! =)
    • +1
      Да, и при этом NULL — это валидное значение для поля _любого_ типа (если поле не определено как NOT NULL, чего в данном случае мы не знаем). То есть, любой типизированный плейсхолдер должен проверять внутри себя is_null и выдавать правильный текст.

      Либо можно сделать как в Котлине, и вставить проверку на NULL в плейсхолдер (то есть условно говоря, “:a” — не допускает NULL, “:a?” — допускает). Но это утяжеление синтаксиса, конечно.
      • 0
        А какой он — правильный текст?
        Для вставки это NULL, тут просто. А при сравнении — что?
        • 0
          Ну, NULL — он и есть нулл, всегда одинаковый. А что, у тебя для сравнения другие плейсхолдеры?

          • 0
            Да, это радует, что в MySQL нет с ним таких заморочек, как, например в Oracle, который пустые строки в нуллы превращает. Скотина (
  • –2
    А не решит ли функция htmlspecialchars() все проблемы со строками, а (int)/(float) — с числами?
    • 0
      Ой.
      Во-первых, функция htmlspecialchars(), как видно из её названия, имеет отношение к HTML, а не к SQL. И в SQL она не поможет, а только навредит. Так что строки надо обрабатывать специально предназначенными для этого функциями.
      Во-вторых, с числами тоже могут быть проблемы (я о них писал)
      В-третьих, кроме строк и чисел есть и другие элементы запроса.
      В-четвёртых, самый главный вопрос состоит не в том, какими функциями форматировать части запроса, а кто это будет делать. Если отдавать форматирование данных на откуп программисту, то он когда-нибудь забудет. Поэтому идея состоит в том, что какие бы функции ни использовались — применять их должна программа, а не программист. В этом суть использования плейсхолдеров.
      • –2
        М… Как навредит?

        $student_name = htmlspecialchars("Heh' DROP TABLE students", ENT_QUOTES);
        $req = "SELECT student_id FROM students WHERE student_name = '$student_name' LIMIT 1";
        
        print($req);
        

        Функция с параметрами превратит этот запрос в безобидный запрос вида… вида, который тут сразу показывается так, как будто там кавычка есть. А на самом деле она заменяется на #039; Это, знаете, как для откручивания хитро закрученного болта можно воспользоваться специальной отвёрткой (всего за $59.95!), а можно — ножом. Результат будет одним и тем же.

        Я согласен, что экранировать данные должна программа. Но если она этого не делает, то что уж тут остаётся? А программеру просмотреть такое несколько затруднительно, чес-слово. Да и при использовании самописных обёрток или PDO — сколько потеряется в производительности? Иногда ведь и 1мс нелишне будет…
        • +2
          Я не понимаю вашей приверженности функции htmlspecialchars().
          Если уж вы хотите руками форматировать строки, то почему не пользоваться специально предназначенной для этого функцией? Функцией, которая не портит данные?
          А после htmlspecialchars в базе останутся все эти html-сущности, которые там даром не нужны.

          Опять же, статью вы, судя по всему, не читали. Иначе поняли бы, что «волшебная» функция htmlspecialchars не всегда помогает:
          $order = htmlspecialchars("name; DROP TABLE students", ENT_QUOTES);
          $req = "SELECT student_id FROM students ORDER BY $order";
          

          Про «программеру просмотреть» и 1мс я, извините, не буду комментировать.
        • +1
          Список «плохих» значений у html и sql качественно разный. Полагаться на их «количественное» сходство — сложноподдерживаемый костыль. Простой пример — «Heh' DROP TABLE students» нам нужно вывести из БД в файл, не предназначенный для вывода в браузер, например в лог. Ваш способ выведет в лог нечитаемую фигню.

          Экранировать данные нужно в момент вывода и строго определенным форматом вывода методом (а ыйл-запросы — это вывод приложения). Безопасность — это побочный эффект экранирования, его основное назначение — сохранение данных в первозданном виде. Для безопасности существует фильтрация.
          • 0
            Ясно, спасибо.
  • 0
    пост добавлен в избранное
    Такое в хозяйстве пригодится =)
  • 0
    Пользую в проектах один из допиленных вариантов DBSimple Котерова, поверх PDO. Плейсхолдеры и белые списки из коробки идут. В общем то да, верный способ обрубить шаловливому хакеру свободу действий на корню.
  • 0
    Понимаю, что несколько в сторону, но глаз зацепился за:
    А ошибки — это всегда плохо.

    It depends, на вскидку:
    1) от дизайна юзкейсов приложения
    В одном случае приложение может перекладывать ответственность на пользователя, репортуя ему напр. о невалидном вводе, в другом — выполнять некий экшн, определенный как дефолтовый.
    2) от деплоймент платформы (dev/prod)
    На боевом экземпляре приложения все возможные ошибки закрываются синим экраном смерти и тихонечко записываются в лог, на разработческом наоборот — выводятся все потроха (колстеки, вардампы и пр)
    • 0
      Вы не совсем поняли, о чём речь.

      1) Пользователь тут вообще не при чём. Задача класса по работе с БД — работать с БД. А не с пользователем. Хотим проверить ввод на валидность — ради бога, ДО любых манипуляций с SQL.
      Собственно, «ошибки» выводимые пользователю — это не ошибки с точки зрения программы, а нормальная работа в штатном режиме. Я же говорил об ошибках, возвращаемых сервером БД, при попытке выполнить запрос.

      2) Вы путаете саму ошибку и сообщение о ней. Про сообщения об ошибках речи не было. Я говорил о самой ошибке. Которая существует независимо от способа информирования о ней.

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