Множественные уязвимости Shop-Script
По привычке, время от времени, просматриваю сообщения об уязвимости скрипта Shop-Script. Компания Артикус, автор этого скрипта, уже давно перестала его поддерживать и продает теперь новую версию, более навороченную и красивую. Тем не менее этот скрипт разошелся по сети в миллионах копий, как легальных, так и нелегальных. А также породил великое множество продолжений и улучшений. Поэтому обнаруженный мною отчет, несмотря на то, что ему уже 2 месяца, будет интересен многим.
В отчете рассматривался Shop-Script Free — это как раз бесплатная версия того, что ходит по интернету огромным тиражом. На Free версии основана наиболее цельная и популярная доработка под названием «Shop-Script Free Lego Edition».
Вывод неутешительный. На 14 апреля в скрипте имелось 27 уязвимостей, позволяющих атакующему получить доступ к базе данных, манипулировать данными и скомпрометировать систему.
В частности ошибки, позволяющие выполнять команды SQL, в обработке параметров, передаваемых в скрипт: (1) add2cart, (2) c_id, (3) categoryID, (4) list_price, (5) name, (6) new_offer, (7) price, (8) product_code, (9) productID, (10) rating, and (11) save_product. Пруфлинк.
Тот же контент, только в профиль
Давно думал над проблемой одинаковых страниц. Ну, почти одинаковых — где один и тот же контент по-разному представлен. Обычно это индексные страницы, типа прайслистов, списков статей, постов или чего-то такого. На большинстве таких страниц есть возможность получить список, скажем, отсортированный по каком-либо критерию — цене, дате публикации и т.д. Таким образом на сайте получаются фактически одинаковые страницы, с одним и те-же или похожим содержанием, но разными URL. Например с сортировкой по-умолчанию, по цене и по названию:
http://www.example.com/products/prices http://www.example.com/products/prices/index/order:price http://www.example.com/products/prices/index/order:title
Мне кажется, что поисковому роботу такое может и не понравится, уж больно это смахивает на попытку заспамить поисковик. Я раздумывал о том, что,наверное, такие страницы, в URL которых указаны подобные параметры неплохо бы, наверное, закрывать от индексирования. Обычными директивами meta в заголовке страницы. Читать полностью »
Список всех-всех контроллеров
Понадобилось мне получить список всех контроллеров приложения, включая контроллеры плагинов. Configure::listObjects, увы, такого не умеет. Этот метод выдает только все контроллеры приложения, но без контроллеров плагинов. Пришлось немного адаптировать метод, предложенный Rob Weaver в гуглогруппе CakePHP. На выходе у моего метода получается объединенный список контроллеров. Основные — как отдает метод listObjects, а принадлежащие плагинам в виде «Plugin.Controller». Можно их поочередно скармливать сразу в App::import. Разве что выбрость те, названия которых на «App» заканчивается, если они не нужны. Будете загружать, помните, что класс «Controller» уже должен быть загружен.
Попробуем почитать между строк
Очередное интервью с представителями Яндекса, на этот раз на Roem.Ru. Никаких особенных откровений, конечно, нет. Что же вы хотели, чтоб яндексоиды всем бесплатно раздали рекомендации, как выбраться на первое место в выдаче по любому запросу? Но все-таки что-то надо говорить и из обтекаемых формулировок можно выстраивать разные забавные предположения. Лично я не завсегдатай SEO-форумов, возможно, все это уже где-то обсуждалось — если баян, извините. Читать полностью »
Контроллеры: загрузка
Удивительно но факт. Если из консольного, например, приложения нужно попользоваться моделью, то, само собой, надо загрузить класс модели.
App::import('Model', 'MyModel');
Но загрузить контроллер так не получится. Получим сообщение об ошибке из-за невозможности найти базовый класс Controller. Вот найти класс Model кейк может, а Controller – нет. Во всяком случае 1.2.5. В версии 1.3 не пробовал.
Поэтому приходится загружать его явно и напрямую:
App::import('Core', 'Controller'); App::import('Controller', 'MyController');
Компоненты: перезагрузка
Не знаю, такую-ли ситуацию имел в виду BorisPlus в своем комментарии. Ну, чем богаты. Вообще не хотел это все описывать, потому как код довольно халтурно написан.
Этот код приложения, изначально крутившийся по Cake 1.1, был переписан для какой-то беты Cake 1.2. С текущей версией, 1.2.5, он работает без проблем, но, возможно, нуждается в чистке.
Эта часть программы выполняющий импорт данных от поставщиков. Поставщики предоставляют данные в CSV формате, но порядок колонок, некоторые значения и т.д., конечно разные. Для осмысленной обработки эти полученные данные надо привести к единообразному виду.
Данные поступают из написанного куцего Datasource, скармливанются компоненту, который и выполняет разбор и приведение к общему виду, потом записываются в нашу таблицу.
Кэш и консоль
За полноценный пост не считается. Так, узелок на память.
В качестве кэша байткода и переменных я использую XCache. Но при запуске консольных приложений, он у меня не работает. Должен или нет, не знаю, не разбирался. Наверное, не должен, если подумать. :-) Просто отметил, что консольные приложения Cake, включая ‘cake bake‘ высыпают кучу ошибок, если XCache используется, как кэш по умолчанию. Поэтому в конфигурации кэша приложения (APP/config/core.php) на CakePHP добавляю маленькую проверку на тип API.
Вот как-то так:
if (PHP_SAPI == 'cli') { Cache::config('default', array('engine' => 'File')); } else { Cache::config('default', array( 'engine' => 'Xcache', 'prefix' => 'mypfx_', 'user'=>'IamAdmin', 'password'=>'c00leztPass')); }
Можно пользоваться константой PHP_SAPI или функцией php_sapi_name(), не важно.
CakePHP 1.2, Content-type, debug, RequestHandler и все-все-все
Как правильно, с точки зрения CakePHP, отвечать на запросы, если ответ требуется не html, а, например, json? Я считаю, что для получения ответа в нужном формате надо воспользоваться компонентом RequestHandler и указать в запросе расширение, в нашем случае ‘.json’. URL для запроса получится какой-то такой:
http://www.oursite.com/controller/action.json
Чтобы Cake не пугался этого расширения и вызывал правильное действие (action), в файл APP/config/routes.php добавим строку:
Router::parseExtensions('json');
Метрическая аналитика
У Яндекса есть полезная система статистики, похожая частью на Google Analytics, частью — на счетчик Liveinternet. Называется Яндекс.Метрика.

Яндекс.Метрика
Все как обычно — устанавливаете код на страницы своего сайта и наблюдаете статистику по посетителям. В отличие от GA, которая обновляется раз в сутки, Метрика показывает результаты практически в реальном времени, как Liveinternet — это гораздо удобнее.
Если сайт рекламируется с помощью Яндекс.Директ и на рекламируемом сайте установлен код для сбора статистики для Директа — это и есть код Метрики! В этом случае можно уже не напрягаться, а смотреть отчеты.
UPDATE нельзя INSERT
Задача очень простая: обновить запись в таблице, а если подходящей записи нет, добавить новую. Кажется вполне тривиальной задачей. Особенно, если условия, по которым выбирается запись для обновления сделать первичным или уникальным ключом.
Эта тема пару месяцев назад обсуждалась в ЖЖ сообществе ru_php, но как-то вяло. Попробую этим небольшим постом обобщить разные методики.
Самый простой метод: сначала SELECT COUNT(*), а потом, по результатам, либо INSERT, либо UPDATE. Если речь идет об обновлении одной записи на этом методе можно остановиться.
Увы, если надо вставить много записей, этот метод начинает изрядно тормозить. Для такого случая у MySQL есть несколько разных методов.
REPLACE — наиболее очевидный и самый неудобный. Сервер пытается вставить новую строку, если возникает ошибка с повторяющимся уникальным ключом, запись удаляется и потом снова добавляется новая. Увы, при этом значения тех полей, которые обновлять не надо сбрасываются в дефолтные. Использовать эту команду можно, если обновляется полностью вся запись, что случается редко. Например, если в таблице есть колонка `created`, содержащая дату создания записи, то REPLACE уже не подходит. Кроме того, в случае, если большинство записей заменяется, а не добавляется, получается конструкция INSERT-(ошибка)-DELETE-INSERT — три запроса!
INSERT … ON DUPLICATE KEY UPDATE … — этот вариант сильно интереснее. Сервер пытается вставить запись, если получает ошибку c дублирующимся ключом, обновляет поля, указанные в после UPDATE. Работает отлично, в документации сказано, что эта конструкция специально задумана для вставки множества записей. Но пара неприятных моментов есть.
- Первый, как мне казалось, довольно экзотичный, но я с ним все-таки столкнулся. В конструкции INSERT можно в качестве источника данных использовать подзапрос SELECT. Но, в случае с INSERT … ON DUPLICATE KEY UPDATE, в подзапросе не должно быть группировки GROUP BY.
- Второй момент для многих неважен. Если записей для обновления сильно больше чем новых, то получается все равно много лишних обращений к БД — ведь вся конструкция это сочетание INSERT-(ошибка)-UPDATE. Лучше, чем с REPLACE, но все же…
Долгое время я пользовался UPDATE, а потом проверял mysql_affected_rows() (в CakePHP это $DataSource->lastAffected(). То есть проверял количество обновленных записей и, если получал 0, добавлял запись. У этого метода есть очень существенный недостаток: если среди данных для обновления встречаются две одинаковых строки, то при повторном UPDATE сервер, как самый умный, не обновляет запись. Соответственно число обновленных записей, affected rows, при построчном переборе, будет равно 0 несмотря на то, что такая строка в таблице есть. Соответственно решение о вставки новой строки будет ошибочным.
Устав мириться с ожидаемым, но неприятным поведением mysql_affected_rows() я порылся в документации на MySQL и нашел еще одну функцию: mysql_info(). Она выдает обычную строчку со статистикой последнего запроса к БД, причем для разных типов запросов строчки разные. Для UPDATE выглядит так:
Rows matched: 40 Changed: 40 Warnings: 0
Зато содержит чрезвычайно ценную информацию: количество записей, которые попадают под условие для обновления. Это те цифры, что после слова ‘matched:‘. Ну, регулярное выражение для получения циферок из строки сами напишите? ;-)
P.S. У меня ощущение deja vu — мне кажется, что я уже писал похожий пост. Перерыл дневники — нет такого. Или это черновик был…



