Класс для безопасной работы с MySQL

Фанат

oncle terrible
Команда форума
Класс сделан для реализации трёх основополагающих идей:
1. Окончательное решение проблемы SQL инъекций.
2. Повышение удобства работы с БД, сокращение рутинных операций.
3. Максимальная гибкость при относительной простоте.
идеи соединялись с некоторым скрежетом и результат получился с некоторыми врождёнными недостатками, увы.
Тем не менее, основной функционал, внедренный на работе в имевшийся ранее класс для работы с БД отработал уже несколько месяцев без особых нареканий.

Самое главное - это, разумеется, безопасность.
Которую я сформулировал из двух правил:

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

Соответственно, класс должен предоставлять возможность следовать этим двум правилам.
Забегая вперёд скажу, что 100% защиты от дурака не получилось, и это оказался единственный компромисс в пользу простоты и удобства за счёт безопасности.

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

Для удобства сделаны хелперы, как в PEAR::DB или DBdimple. Ничего нового, просто возможность сразу получить результат запроса в желаемом виде.
Плюс пара кастомных плейсхолдеров, которые облегчают формирование сложных операторов.
пример:
PHP:
$data = array('offers_in' => $in, 'offers_out' => $out);
$sql = "INSERT INTO stats SET pid=?i,dt=NOW(), ?u ON DUPLICATE KEY UPDATE ?u";
$this->db->query($sql,$pid,$data,$data);
Простота и гибкость.
Основная идея состоит в том, что мы работаем только с SQL. Никаких методов, забирающих себе функциональность SQL (и ограничивающих её).
Всё что можно, автоматизируется с помощью плейсхолдеров:
PHP:
$name = $db->getOne('SELECT name FROM table WHERE id = ?i',$_GET['id']);
$data = $db->getInd('id','SELECT * FROM ?n WHERE id IN ?a','table', array(1,2));
$data = $db->getAll("SELECT * FROM ?n WHERE mod=?s LIMIT ?i",$table,$mod,$limit);
Т.е. большинство стандартных запросов пишутся тупыми однострочниками.

Для любых же нестандартных случаев у нас НЕТ специальных методов. Всё делается той же самой тупой ручной сборкой SQL-я как и раньше.
Но из безопасных кусков.

Здесь на первый план выходит метод parse(), которому по барабану, что парсить - хоть целый запрос, хоть его часть.
PHP:
$w = array();
$where = '';
if ($one) $w[] = $db->parse("one = ?s",$one);
if ($two) $w[] = $db->parse("two IN (?a)",$two);
if ($tre) $w[] = $db->parse("tre <= ?i",$tre);
if (count($w)) $where = "WHERE ".implode(' AND ',$w);
$data = $db->getAll("SELECT * FROM table $where LIMIT ?i,?i", $start,$per_page);
Переменная $where - тот самый компромисс, о котором я говорил выше.
Да, это переменная, подставляемая прямо в запрос.
Да, у пользователя должно хватать ума, чтобы подставлять только те переменные, которые вышли из рук метода parse(), и были составлены с соблюдением всё тех же правил.
но если соблюдать это нехитрое условие, код работы с SQL становится максимально компактным и в то же время максимально безопасным.

Вводная статья на хабре: http://habrahabr.ru/post/165069/
Документация лежит тут: Класс для защиты от SQL инъекций, с примерами и пояснениями
Код https://github.com/colshrapnel/safemysql/blob/master/safemysql.class.php

Очень бы хотелось фидбека по двум линиям:
Первая - это, естественно, функционал. Вопросы о применению, идеи по улучшению.
Вторая - синтаксис. Я не великий классописатель, и все новейшие достижения программистской мысли прошли мимо меня стороной. поэтому буду рад привести кд в соответствие самым модным веяниям. единственное условие - сохранение принципа plug and play - портабельность и сохранение низкого порога вхождения, не требующего возведения поддерживающего фреймворка.
 
Последнее редактирование:

флоппик

promotor fidei
Команда форума
Партнер клуба
$where нарушает принцип инкапсуляции. parse() возможно, должен быть статическим методом, так как для его использования тебе не нужен инициализированный объект (как и реальный коннект к бд).
Что бы избавится от $where, я бы предложил тебе ее встроить в объект и завести плейсхолдер для нее, что то вроде:
PHP:
$db->parse("one = ?s",$one)->parse("two IN (?a)",$two)->parse("tre <= ?i",$tre);
$data = $db->getAll("SELECT * FROM table #where LIMIT ?i,?i", $start,$per_page);
Таким образом сократится и код, и логика станет более предсказуемой.
 

Фанат

oncle terrible
Команда форума
я не готов к таким вещам.
я уже много раз сталкивался с тем, что "гладко было на бумаге, да забыли про овраги". И всё работает ровно до тех пор, пока #where строится по логике не сложнее, чем implode(' AND ',$w); а там - опять накручивать синтаксис, писать SQL на РНР. Случаев-то сильно больше, чем просто сборный where. Я слишком уважаю SQL, чтобы загонять его в прокрустово ложе своего представления о нём.
Возможно, я и неправ в своем подходе, и класс окажется ненужен никому - нубасам не получится осилить даже его, а у папок и так есть свои ормы с доктринами. Но менять ключевую идеологию под последних тем более глупо. Это как Файрфокс, который кинулся копировать Хром во всём, и потерял лицо. Пусть он был бы хуже, но он был бы Фоксом. Со своей аудиторией.

Любое усложнение логики ведёт, во-первых, к ограничениям, а во-вторых - к увеличению документации.
коллеги на работе до сих пор даже список из 5 плейсхолдеров и 6 хелперов запомнить не могут. А все они умнее меня 100%. Но новое всегда сложно прививается. И уж тем более кодить, сидя носом в справочник - неудобно. Инструмент, к которому требуется толстый ануал - не инструмент, а обуза.

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

Ручная сборка запроса - это ключевая идеология класса. Если я отступлю от неё, то своё лицо потеряю, а до большого орма все равно не дорасту. Зачем тогда?
Здесь идея как раз и состоит в том, что есть набор базовых кирпичей для составления запросов, никак при этом не ограничивающий пользователя.
Да, у меня нет метода insert, принимающего всего два параметра - имя таблицы и данные.
PHP:
$db->insert($table,$data);
И мне приходится для каждого инсёрта писать лишние INTO и SET.
PHP:
$db->query("INSERT INTO ?n SET ?u",$table,$data);
Но зато когда мне понадобился ON DUPLICATE - это не стало камнем преткновения. Я легко его написал, используя те же самые кирпичи:
PHP:
$sql = "INSERT INTO stats SET pid=?i,dt=NOW(), ?u ON DUPLICATE KEY UPDATE ?u";
$this->db->query($sql,$pid,$data,$data);
То же самое касается и сборных запросов.
 

Фанат

oncle terrible
Команда форума
Хотя подобные идеи и приходили мне в голову.
Но обязательно без специального плейсхолдера для WHERE.
Тут если только собирать результаты парсов внутри класса, и вставлять по очереди на место специальных именованных плейсхолдеров.
Но всё равно не получилось придумать.
Тот же множественный инсерт ломает все рамки.

В общем, идея освободиться от единого, цельного запроса, которым связаны руки в ПДО, например, оказалась столь прекрасной, что я не готов от неё отказаться. Хотя вполне допускаю такая ещё может появиться.
 

С.

Продвинутый новичок
Согласен, что в таком виде, как есть, без "инкапсуляции" WHERE все логично и последовательно. Я в своем фреймворке путем проб и ошибок в течение нескольких лет пришел практически к такому же классу.

Есть одно дополненение, которое я считаю достаточно часто используемым, чтобы его игнорировать. В семействе методов getInd* надо разрешить более одного индекса, чтобы сразу строить многомерный массив. На практике более двух индексов еще ни разу не понадобилось, но с двумя получается крайне удобно.
 

Фанат

oncle terrible
Команда форума
честно говоря, я даже getIndСol хочу выкинуть.
с этой функцией я увлёкся, и забыл, что автоматизировать надо только очень часто используемые операции.
А такие случаи, как индексированная колонка (явно под имплод как значений так и ключей) или заполнение двумерного массива всречаются очень редко, и их вполне можно зарядить внутри обычного цикла.
Всё-таки, количество методов - это очень важный параметр.
 

С.

Продвинутый новичок
Согласен, *Col методы совсем лишние. И под индексированный массив тоже необязательно выделять отдельные методы. У меня используются те же get*, но необязательным параметром задается индексирущая колонка или колонки.
заполнение двумерного массива всречаются очень редко, и их вполне можно зарядить внутри обычного цикла
Ну в обычном цикле можно все сделать, и результаты запроса перебрать. Однако мы инкапсулирем его внутрь и выдаем массив потому, что так удобнее.

Двумерный массив это не так редко. Например список элементов, сгрупированный по категориям -- совсем не экзотика. Да можно отсортировать в запросе и в одном цикле плоский массив перебрать со счетчиком категорий. Но намного удобнее, семантичнее и симпатичнее два цикла один в другом. За удобство и чистоту алгоритмов же боремся как никак.
 

С.

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

~WR~

Новичок
Неплохо, но есть одна фундаментальная ошибка.
При сборке запроса из кусочков не учитывается такой момент, что в тексте от пользователя тоже могут придти плейсхолдеры.
PHP:
$one = 'some text with ?s placeholders';

$where = $db->parse("one = ?s",$one); 
$data = $db->getAll("SELECT * FROM table WHERE $where LIMIT ?i,?i", $start,$per_page); //'Number of args does not match' error
 

Фанат

oncle terrible
Команда форума
охренеть
~WR~, я твой должник

главное, пока никаких идей.
или стоп. парсе должна возвращать удвоенные вопросительные знаки.
хотя это тоже косяк в общем.
 
  • Like
Реакции: Gas

С.

Продвинутый новичок
Неплохо, но есть одна фундаментальная ошибка.
При сборке запроса из кусочков не учитывается такой момент, что в тексте от пользователя тоже могут придти плейсхолдеры.
Нет ошибки. После работы parse() в getAll() того "кусочкого" плейсхолдера уже нет.
 

Фанат

oncle terrible
Команда форума
Нет ошибки. После работы parse() в getAll() того "кусочкого" плейсхолдера уже нет.
PHP:
$one = 'some text with ?s placeholders';
$where = $db->parse("one = ?s",$one);
$sql  = $db->parse("SELECT * FROM table WHERE $where");
//Number of args (0) doesn't match number of placeholders (1) in
// [SELECT * FROM table WHERE one = 'some text with ?s placeholders']
А ведь кто-то давал мне парсер, в котором не учитываются вопросы внутри строк...
Но это всё равно полумера.
 

С.

Продвинутый новичок
А ну да. Но я не считаю это большой бедой. Зкранировать их вместе с кавычками.
 

Gride

Новичок
1. А как плейсхолдер ?n сработает при связывании таблиц, если, например, задать в формате database.table.field ? По идее, базе надо скормить `database`.`table`.`field`, если я синтаксис не путаю. Мануал об этом молчит.
2. Сделай пожалуйста ссылку на мануал из самого phpfaq, чтоб не с форума заходить.
 

Фанат

oncle terrible
Команда форума
1. А как плейсхолдер ?n сработает при связывании таблиц, если, например, задать в формате database.table.field ? По идее, базе надо скормить `database`.`table`.`field`, если я синтаксис не путаю. Мануал об этом молчит.
Хороший вопрос.
в текущем варианте только
PHP:
?n.?n.?n
скармливая, соответственно, три переменных.

2. сделал
 
Сверху