Repository navigation
fix(notifications): не исполнять теги из данных покупателя при сборке письма (#888) - #899
Conversation
… письма (#888) Тело письма собирается Fenom-чанком вместе с полями заказа, а затем прогоняется через processElementTags. С установленным pdoTools $modx->getParser() — не разбор MODX-тегов, а Fenom, поэтому из комментария покупателя исполнялись и [[++setting]], и {$_modx->runSnippet(...)}: в Fenom pdoTools через $_modx доступны runSnippet() и runProcessor(). На стенде по комментарию заказа подтверждено исполнение сниппета, чтение системной настройки и работа Fenom-модификатора. Второй вектор — тема письма. processSubject() подставлял данные покупателя в строку темы, а buildSubject() компилировал эту строку как шаблон через '@inline '. Тема вида «Заказ {num}: {order_comment}» исполняла код из комментария. Данные теперь уходят в рендер с подменёнными открывающими последовательностями тегов и возвращаются к исходному виду уже после парсера. Метка разовая на каждое письмо, иначе её можно было бы вписать в комментарий и получить обратно [[ или {. Обратная совместимость полная: проверено, что в самом чанке продолжают работать Fenom-переменные и модификаторы, MODX-настройки, MODX-лексикон, кэшируемые и некэшируемые сниппеты. processSubject() удалён: плоские {key} подставляет EmailChannel последним шагом, когда парсеров за ним уже нет. Fenom понимает только {$key}, так что подстановка нужна — на ней держатся существующие темы вида «Заказ {num}». Заодно убран str_replace без фигурных скобок в fallback-ветке: он подменял любое вхождение имени ключа в тексте. Utils::sendEmail() получает тело собранным, поэтому прогон парсера по нему убран — вызывающий код сам решает, что разбирать, пока в строке ещё нет чужого текста. Внутри MS3 метод не используется. Поля покупателя в штатных чанках писем выводятся через |e: скрипты в письме не исполняются, но <a href> и <img src> работают, то есть покупатель мог подсунуть менеджеру ссылку или трекинг-пиксель.
…#888) По ревью PR. Защита стояла только в buildBody, и тема оставалась открытой для Fenom- и MODX-форм обращения к данным: flat {order_comment} -> текст, ок fenom {$order_comment} -> ИСПОЛНЕНО nested {$address.first_name} -> ИСПОЛНЕНО modx [[+order_comment]] -> ИСПОЛНЕНО Причина в том, что CoreTools::getChunk() после Fenom сам подставляет [[+key]] и прогоняет остаток через разбор MODX-тегов. Значение, вставленное Fenom в вывод, доезжало до парсера уже как шаблон — то есть в теме работал тот же класс дырки, который PR закрывает в теле. Вторая находка ревью — регрессия обратной совместимости. Удалив processSubject(), я отдал плоские {key} прямо в @inline: Fenom спотыкается на {num} при компиляции, а pdoTools на ошибке подставляет пустой шаблон, так что тема со смесью плоского ключа и любой Fenom-конструкции уходила пустой. Заказ {num} / {$status_name} OLD: Заказ 2610/1 / Новый NEW: (пусто) Заказ {num}: {'ms3_frontend_comment'|lexicon} OLD: Заказ 2610/1: Коммент. NEW: (пусто) Оба лечатся одним механизмом в buildSubject(): плоские {key} маскируются в самом шаблоне, значения плейсхолдеров нейтрализуются, и уже после парсера сначала возвращаются открывающие последовательности, затем разворачиваются значения плоских ключей. Парсеров за этим нет, поэтому значение шаблоном не станет, а Fenom больше не спотыкается на {num}. Разворачивание через strtr, так что значение с {другим_ключом} внутри не попадает под следующую подстановку. Метка теперь только из цифр: модификатор вроде |upper её не меняет и разворачивание не проходит мимо. От |truncate это не спасает — обрезанный маркер развернуть нельзя, ограничение описано в коде. Сторож sendEmail() режет метод по следующему объявлению функции вместо окна в 1200 символов.
|
Оба критичных замечания воспроизвёл и закрыл. Коммит Критичное 1 — инъекция в темеПодтвердилось полностью. Я ошибся, решив, что в pdoTools-ветке Прогон реального
Остальные нагрузки из ревью в теме Критичное 2 — BC темы со смесьюТоже подтвердилось, и причина ровно та, что в ревью: Fenom спотыкается на
Моё утверждение в описании PR про Как сделаноРовно одним механизмом, как и предлагалось. В
Некритичные
Гейты локально: PHPUnit 820/820 (2899 assertions, 1 чужая deprecation), smoke 123/123, PHPStan по |
) Маркер строится как метка + индекс + метка, то есть заканчивается меткой. restoreTags() шёл раньше разворачивания и искал метку с цифрой 1 или 2, так что стоящая сразу за маркером цифра складывалась с хвостом в искомую последовательность: хвост съедался, strtr уже не находил маркер и в тему уезжал мусор. Заказ {num}1 шт -> Заказ 99669582318436946930[[ шт Заказ {num}2026 -> Заказ 98649116705278225930{026 Цифра приходит и из текста темы, и из значения, вставленного Fenom рядом с плоским ключом. Парсеров за этим уже нет, так что безопасность не страдала. Лечится переменой порядка двух последних шагов: сначала значения плоских ключей, потом открывающие последовательности. После разворачивания висячей метки в строке не остаётся — нейтрализованные значения держат её только в составе пары, а сырое значение плоского ключа 18-значную метку содержать не может. Сторож порядка перевёрнут соответственно, в unit добавлены суффиксы сразу за плоским ключом.
|
Воспроизвёл и закрыл. Коммит Диагноз точный: маркер заканчивается меткой, Сделал как предложено — перемена порядка двух последних шагов: сначала Сторож перевёрнут на Прежние проверки не поехали: четыре формы инъекции в теме остаются текстом, смесь плоского ключа с Fenom по-прежнему собирается. Гейты локально: PHPUnit 827/827 (2913 assertions, 1 чужая deprecation), smoke 123/123, PHPStan по |
Closes #888
Проблема шире, чем в issue
EmailChannel::buildBody()собирает тело Fenom-чанком вместе с полями заказа, а потом прогоняет готовую строку черезprocessElementTags(). Ключевой момент: при установленном pdoTools$modx->getParser()— это не разбор MODX-тегов, а Fenom. Стектрейс:В Fenom pdoTools через
$_modxдоступныrunSnippet()иrunProcessor(). То есть из комментария покупателя исполняется не только[[++setting]], но и произвольный код шаблонизатора.Вход — комментарий, имя и адрес в публичном оформлении заказа. Триггер — отправка любого письма по заказу. pdoTools формально не в
requires(_build/build.php:119закомментирован), но без него Fenom-чанки писем не рендерятся вовсе, так что на рабочих сайтах он стоит всегда.Второй вектор — тема письма
StatusChangedNotification::processSubject()подставлял данные покупателя в строку темы, аbuildSubject()компилировал эту строку как шаблон через'@INLINE ':Третье место
Utils::sendEmail()(Utils.php:199-200) содержал те же два вызова парсера по готовому телу. Внутри MS3 не используется — это публичный метод для расширений.Проверка на стенде
Один и тот же комментарий заказа,
tpl.msEmail.new.manager, до и после правки:{$_modx->runSnippet("ms3Probe888")}— сниппет возвращает метку, которой нет в нагрузке[[++emailsender]]— настройка прочитана{"zz"|upper}— Fenom-модификатор<img src=x onerror=alert(1)><a href="http://evil"><b>Боб</b>После правки комментарий в теле письма выглядит так:
Решение
Тело: данные нейтрализуются вокруг парсера
Перед рендером в значениях плейсхолдеров рекурсивно подменяются открывающие последовательности
[[и{, после прохода парсера они возвращаются на место. Метка разовая на каждое письмо — с постоянной покупатель вписал бы её в комментарий и получил обратно[[или{после разворачивания.Такой порядок выбран ради обратной совместимости: прямое удаление
processElementTagsсломало бы чанки магазинов, написанные на MODX-тегах. Проверено на чанке в «старом» стиле — после правки в самом шаблоне по-прежнему работает всё:Тема: тот же механизм
processSubject()удалён — он подставлял данные в текст шаблона до компиляции.Первая редакция PR заменяла его подстановкой после компиляции, но этого оказалось мало. Ревью нашло два дефекта, исправлены в
0f3b1455и6f468b8e:CoreTools::getChunk()у pdoTools после Fenom сам подставляет[[+key]]и прогоняет остаток через разбор MODX-тегов, поэтому значение, вставленное Fenom через{$order_comment}или{$address.first_name}, доезжало до парсера шаблоном. Тема закрыта тем же механизмом, что и тело: значения нейтрализуются.{key}, отданные в@INLINEкак есть, валили компиляцию Fenom, а pdoTools на ошибке подставляет пустой шаблон — тема со смесью{num}и любой Fenom-конструкции уходила пустой. Теперь плоские ключи маскируются в самом шаблоне и разворачиваются после парсеров, перед восстановлением открывающих последовательностей.Подстановка плоских ключей нужна, потому что Fenom понимает только
{$key}. Существующие темы работают в обоих синтаксисах:Заодно убран
str_replace("{$key}", …)из fallback-веткиbuildSubject()— без фигурных скобок он подменял любое вхождение имени ключа в тексте темы.Экранирование
|eна поля покупателя и снимок заказа вms3_email.tplиms3_email_new_manager.tpl. Скрипты в письме не исполняются, но<a href>и<img src>работают, то есть покупатель мог подсунуть менеджеру ссылку или трекинг-пиксель. Автоэкранирования в pdoTools нет — двойного экранирования не будет;format->price()иformat->weightWithUnit()HTML не содержат, их не трогал.Важно: экранирование само по себе от исполнения не защищает —
htmlspecialcharsне трогает[и{. Нужны оба механизма.Тесты
tests/Unit/Notifications/EmailChannelTagHandlingTest.php— нейтрализация не оставляет открывающих последовательностей, round-trip восстанавливает исходный текст, обход идёт по вложенным массивам и не портит типы, метка разная на каждое письмо, плоская подстановка берёт только{key}и не переинтерпретирует значение.tests/EmailTagInjectionGuardTest.php— сторожит порядок операций в исходниках (нейтрализация до рендера, восстановление после парсера, подстановка темы после парсера), отсутствиеprocessSubject, отсутствие парсера вUtils::sendEmail()и наличие|eу каждого вывода полей покупателя в штатных чанках.На коде до правки smoke падает первым же утверждением, unit — все 18 кейсов.
Гейты локально: PHPUnit 818/818, smoke 123/123, PHPStan по
src/Notifications/иUtils.phpчисто.Для dev-стендов
Чанки писем правятся в
elements/, а на стендах они обычно лежат в БД (static=0) — после обновления нужно перезалить кодtpl.msEmailиtpl.msEmail.new.manager.