Челендж на 2026

ArbNet
На сайте с 27.10.2019
Offline
152
#541
Александр Воробьев #:
Сам по себе подход такой используется практически везде (и не только в PHP). Он на поверхности и вполне естественный т.к. обуславливается тем как работает PHP. Различия только в конкретике реализации. Тут уже лучше погружаться в код (если есть интерес в обсуждении) - видео (тем более если будет без обратной связи) не имеет смысла, т.к. нужны вопросы - ответы. Так что если есть вопросы по реализации - спрашивай.
Ну, мне по сути всё понятно уже. Сделать такое в принципе не проблема. Но для моего подхода такой способ всё равно не подходит, я же сделал инструмент для обычных людей, далёких от программирования, им не зачем изучать всякого рода шаблнизаторы для компиляции страниц и тп. Да, без определённых знаний конечно не обойтись, но я как мог упростил создание страниц, по моему нет ничего проще чем простой HTML, а у меня XML с некоторыми дополнительными возможностями, вот и всё.
Александр Воробьев
На сайте с 03.02.2020
Online
64
#542
ArbNet #:
Я же говорю бездари.. давно уже не воспринимаю современных учёных с точки зрения истинности.
Пофиг. предлагаю не обсуждать. я сам пишу то так то так. Но больше склоняюсь к написанию через "е" по причине того, что так употребляется (по моим наблюдениям) наиболее часто. А у нас в русском языке принимают и даже меняют нормы если большинство применяет определенным образом (как пример род слова "кофе").
master32
На сайте с 09.02.2022
Offline
131
#543
Александр Воробьев #:
Состояние на сегодня на github
скормил своему агенту и вот что он ответил:

Аудит joke-php/templator на коммите eecfa4b7dec3897f24dafa30cd450abada15fa0b

Короткое резюме

Коммит eecfa4b7dec3897f24dafa30cd450abada15fa0b реализует шаблонизатор с компиляцией AST в PHP и последующим include кэшированного файла. Ключевой риск не в классическом eval , а в цепочке compileFile() → сырой TextNode → include $cache->path : при неограниченном пути к шаблону это даёт чтение произвольных файлов, а при попадании в шаблон текста с <?php ... ?> — исполнение PHP-кода из “текстового” шаблона.

Топ-5 критичных/высоких проблем в этом коммите:

  1. LFI/path traversal через includeFile() / compileFile() : путь к шаблону никак не ограничен корнем шаблонов и читается напрямую через file_get_contents() .
  2. RCE через “обычный текст” шаблона: TextNodeHandler возвращает сырой текст без экранирования/нейтрализации PHP-тегов, а кэш затем исполняется через include .
  3. Расхождение compile vs render на отсутствующих ключах: compiled-mode генерирует прямой доступ вида $context['user']['name'] , тогда как render-mode идёт через resolveValue(..., $default) ; это ломает предсказуемость и приводит к предупреждениям PHP на пустых данных.
  4. Потеря внешних локальных переменных в вложенных foreach при компиляции: внутренний цикл затирает список localVars , из-за чего обращения к переменным внешнего цикла компилируются уже как $context[...] .
  5. {% csrf %} создаёт ложное ощущение защиты: код выводит только сырой токен, а не hidden-input; кроме того, compiled/render paths ведут себя по-разному при недоступных сервисах.


Нештатный путь к шаблону открывает чтение произвольных файлов.
TemplateEngine::includeFile() принимает $file, создаёт FileRelatedCache, а при отсутствии кэша зовёт compileFile($file, $context); compileFile() проверяет лишь file_exists($path) и затем делает file_get_contents($path). Ни template-root, ни realpath()-проверки, ни allowlist расширений тут нет.

Минимальный сценарий:

php

// Плохо: путь приходит извне
$engine->includeFile($_GET['tpl'], []);

// /page?tpl=../../../../etc/passwd



Короткая рекомендация: ограничить все template paths жёстким каталогом шаблонов, проверять realpath($path) на префикс корня, запрещать wrappers и принимать только ожидаемые расширения. Если библиотека сознательно низкоуровневая, это ограничение всё равно лучше реализовать в самом движке, а не делегировать приложению.

Сырый TextNode делает PHP-теги в шаблоне исполняемыми.
TextNodeHandler::compile() возвращает $node->content без изменений, а TemplateEngine::includeFile() затем исполняет кэшированный файл через include. Это означает, что шаблонный текст <?php ... ?> не выводится как текст, а реально выполняется в PHP-процессе.

Минимальный сценарий:

php

file_put_contents('/tmp/tpl.php', '<?php echo "PWNED"; ?>');
$engine->includeFile('/tmp/tpl.php', []);
// Ожидание от шаблонизатора: вывести literal text
// Фактически: выполнить PHP-код



Короткая рекомендация: при компиляции text nodes генерировать безопасный вывод, например <?php echo '...'; ?> через var_export($node->content, true), либо заранее отклонять любые PHP opening tags во входных шаблонах. В текущем виде это главный архитектурный дефект безопасности.

Compiled-mode и render-mode расходятся на отсутствующих ключах.
compileVarAccess() строит прямой код вида $context['user']['name'], а PrintNodeHandler слепо вставляет его в htmlspecialchars((string){$code}, ...); аналогично IfHandler делает (bool)($this->compileVarAccess(...)), а EachHandler компилирует foreach ({$arrayAccess} as ...). В интерпретаторе же везде используется resolveValue(..., $default), который возвращает default при отсутствии пути. В PHP 8.x чтение неопределённого ключа массива — это E_WARNING, а доступ к offset у null тоже даёт предупреждение.

Минимальный сценарий:

php

// Шаблон
{{ user.name }}

// Что генерирует компилятор по сути:
<?= htmlspecialchars((string)$context['user']['name'], ENT_QUOTES, 'UTF-8'); ?>

// Что делает render-mode:
$value = resolveValue($context, 'user.name', '', 'PrintNode'); // => ''



Короткая рекомендация: вынести общий helper safe-access и использовать его и в compiled-mode, и в render-mode. Иначе библиотека остаётся непредсказуемой: один и тот же шаблон на тех же данных ведёт себя по-разному в двух режимах.

Вложенный foreach теряет переменные внешнего цикла при компиляции.
В EachHandler::compile() после разбора аргументов список локальных переменных жёстко заменяется на [$valueVar] и, опционально, $keyVar; старые localVars не наследуются. В EachHandler::render() наоборот строится $iterationContext = $context и туда добавляются локалы; то есть интерпретатор внешнюю область видимости сохраняет, а компилятор — нет.

Минимальный сценарий:

twig

{% foreach user in users %}
  {% foreach role in roles %}
    {{ user }}
  {% /foreach %}
{% /foreach %}



Почему ломается:

php

// Внутри inner-foreach localVars уже только ['role'],
// поэтому {{ user }} компилируется не в $user, а в $context['user'].



Короткая рекомендация: вместо $localVars = [$valueVar]; использовать накопление области видимости, например array_values(array_unique([...$localVars, $valueVar, $keyVar])). Это исправит и compile/render parity, и вложенные циклы.

{% csrf %} не вставляет отправляемое поле формы и даёт ложную семантику безопасности.
Docblock обещает “генерацию скрытого поля или вывод токена”, но реализация compile() и render() фактически возвращает только строку токена: в compiled path делается echo $tokenManager->getServerToken($request);, а в render path возвращается сам токен. Браузер не отправляет “просто текст” из <form> как form field, поэтому такой API легко использовать неправильно и остаться без реальной CSRF-защиты.

Минимальный сценарий:

html

<form method="post">
  {% csrf %}
  <button>Save</button>
</form>



Фактический эффект:

html

<form method="post">
  3c6f4b...
  <button>Save</button>
</form>



Короткая рекомендация: либо сделать директиву полноценным hidden-input (<input type="hidden" name="_token" ...>), либо честно переименовать её в csrf_token и заставить разработчика явно оборачивать значение. Дополнительно стоит выровнять compile/render null-handling: сейчас compiled path сервисы проверяет, render path — уже нет.

Лексер имеет очевидную деградацию по производительности на больших шаблонах.
findNext() на каждой итерации перебирает все token descriptors и для каждого вызывает strpos($template, $descriptor->open, $pos); сам автор оставил TODO про производительность именно на этом месте. При двух маркерах проблема не катастрофична, но на больших шаблонах с длинными текстовыми сегментами это лишняя повторная работа.

Минимальный сценарий:

php

$template = str_repeat('a', 1_000_000) . '{{ user.name }}';



Короткая рекомендация: заменить “много strpos на каждом шаге” на линейный сканер или единый regex/token automaton. Это не security issue, но для engine-level кода уже заметная structural cost.

Инструментальная картина и покрытие тестами

В коммите есть PHPUnit, PHP-CS-Fixer и PHPStan: это видно по require-dev и composer scripts (test, stan, fixer, check). PHPStan настроен на level 8, PHPUnit — с coverage-репортами по ./src, а тестовое дерево покрывает Lexer, Parser, Handler, Compiler, Render, TemplateEngine и config/provider слои.

Но есть три явных пробела. Первый: Psalm не подключён вообще — ни зависимостью, ни конфигом. Второй: .phpstan.neon.dist исключает tests, хотя script stan формально запускает анализ и по tests; это снижает реальный эффект “analyse src tests”. Третий: в корне этого SHA не видно .github/workflows, то есть автоматический CI на самом коммите не просматривается.

По имеющемуся списку тестов видно, что unit-тесты по слоям есть, включая CsrfHandlerTest.php, IfHandlerTest.php, EachHandlerTest.php, PrintNodeHandlerTest.php и TemplateEngineTest.php. Однако по самим именам/видимым точкам входа не просматривается отдельного end-to-end покрытия на самые опасные сценарии этого аудита: literal <?php ... ?> внутри text node, path traversal через template path, parity compiled/render на отсутствующих ключах и parity nested-foreach по внешним локальным переменным. Это не доказательство полного отсутствия кейсов, но как минимум они не выделены в явные отдельные test targets.

Команды, которые разумно запускать для воспроизведения на локальном checkout этого SHA:

text

composer install
composer stan
composer fixer
composer test
# Psalm в этом коммите не настроен; для него сначала нужно добавить зависимость и конфиг.



Команды stan, fixer и test подтверждены в composer.json; отсутствие Psalm — тоже.

Вывод

Главная проблема этого коммита — не синтаксис шаблонов как таковой, а то, что движок компилирует текст шаблона в исполняемый PHP-файл и затем делает include, одновременно позволяя читать шаблон по произвольному пути. В сочетании это даёт сначала LFI/чтение любых локальных файлов, а затем и RCE при наличии PHP-кода в source text. Остальные важные проблемы — несогласованность compiled/render semantics, сбой областей видимости в вложенных циклах и misleading CSRF API.

Если исправлять по приоритету, то порядок такой: закрыть template path, запретить/нейтрализовать PHP в text nodes, унифицировать safe-access для compile/render, починить наследование localVars в foreach, переопределить семантику {% csrf %}, а затем уже добирать CI/Psalm/perf-оптимизации лексера. Это даст наибольший выигрыш и по безопасности, и по корректности, и по сопровождению.

Александр Воробьев
На сайте с 03.02.2020
Online
64
#544
ArbNet #:
Ну, мне по сути всё понятно уже. Сделать такое в принципе не проблема. Но для моего подхода такой способ всё равно не подходит, я же сделал инструмент для обычных людей, далёких от программирования, им не зачем изучать всякого рода шаблнизаторы для компиляции страниц и тп. Да без определённых знаний конечно не обойтись, но я как мог упростил создание страниц, по моему нет ничего проще чем простой HTML, а у меня XML с некоторыми дополнительными возможностями, вот и всё.

А причем тут "люди".?  Это все работает под капотом. разработчику (а именно эту роль выполняет человек создающий сайт) не обязательно даже будет знать как это работает. очень условно у него будет метод подключения страницы (читай шаблона страницы). и он даже может не знать, что там вообще есть кеширование. Это, на мой взгляд, правильная работа хорошего инструмента. т.е. как пример псевдокодм

ShowPage('index.xml');

Т.е. пользователь сказал что он хочет видеть страницу. Остальное не его проблемы.  Даже могу продемонстрировать на твоем фреймворке. (на той версии что у меня и, оговорюсь, я уже подзабыл его детали реализации, так что только по беглому поиску и взгляду) Правда тут с некоторым допущением у тебя сразу логика запускается некоторая на основе разбора xml. Но ее можно упаковать (в этом случае правда больше подойдет "классический кеш" (наподобии как описан в PSR)

class Manual {
   final public function Load(string $name){
//..некоторый код
      if(file_exists($way)){
        $cache = new FileRelatedCache('/tmp',$way, 86400);
        if ($cache->exsists()) {
          // достаем из кеша
        } else {
          $xml=simplexml_load_file($way);
          if($xml){
            // обработка
            $cache->save($data);
          }
        }
      }
//..некоторый код
   }
}

Т.е. все происходит внутри твоего метода. Это можно и не показывать пользователю, только дать ему инструмент сброса кеша по требованию

Александр Воробьев
На сайте с 03.02.2020
Online
64
#545
master32 #:
LFI/path traversal через includeFile() / compileFile() : путь к шаблону никак не ограничен корнем шаблонов и читается напрямую через file_get_contents() .

Да это пока сознательно. Сейчас вынашиваю мысль добавить в фрейморк сервисы для работы  файловой системой и убрать это все туда.  Но пока не решил когда это делать, если до релиза шаблонизатора не сделаю - то в шаблонизатор пока по сути "костыль" добавлю не дающий выползать ща пределы.

master32 #:
RCE через “обычный текст” шаблона: TextNodeHandler возвращает сырой текст без экранирования/нейтрализации PHP-тегов, а кэш затем исполняется через include .

Тоже понимаю. Пока идеология - не брать лишнюю ответственность. Т.е. разработчик должен позаботится о подготовке данных. Тут честно пока для себя не принял решение окончательное. 

master32 #:
Расхождение compile vs render на отсутствующих ключах: compiled-mode генерирует прямой доступ вида $context['user']['name'] , тогда как render-mode идёт через resolveValue(..., $default) ; это ломает предсказуемость и приводит к предупреждениям PHP на пустых данных.

Пасиб. подумаю.

master32 #:
Потеря внешних локальных переменных в вложенных foreach при компиляции: внутренний цикл затирает список localVars , из-за чего обращения к переменным внешнего цикла компилируются уже как $context[...] .

Тут идея такая то все же localVars это переменные блока. тоже еще у меня нет 100% видения как правильно. Вероятно уже по обкатаю на том же проекте форума - может что пойму для себя. :)

master32 #:
Короткая рекомендация: вынести общий helper safe-access и использовать его и в compiled-mode, и в render-mode. Иначе библиотека остаётся непредсказуемой: один и тот же шаблон на тех же данных ведёт себя по-разному в двух режимах.

Интересно. подумаю

master32 #:
{% csrf %} не вставляет отправляемое поле формы и даёт ложную семантику безопасности.
Docblock обещает “генерацию скрытого поля или вывод токена”, но реализация compile() и render() фактически возвращает только строку токена: в compiled path делается echo $tokenManager->getServerToken($request);, а в render path возвращается сам токен. Браузер не отправляет “просто текст” из <form> как form field, поэтому такой API легко использовать неправильно и остаться без реальной CSRF-защиты.

Тут из собственного опыта. На самом деле надо и так и так. Скорее всего будет добавлена директива типа csrf-input.

master32 #:
Короткая рекомендация: заменить “много strpos на каждом шаге” на линейный сканер или единый regex/token automaton. Это не security issue, но для engine-level кода уже заметная structural cost.

Да. TODO там не спроста :)  обязательно этот вопрос будет изучаться.

master32 #:
покрытия на самые опасные сценарии этого аудита: literal <?php ... ?> внутри text node, path traversal через template path, parity compiled/render на отсутствующих ключах и parity nested-foreach по внешним локальным переменным

Об этом, если честно, даже не подумал...

ArbNet
На сайте с 27.10.2019
Offline
152
#546
Александр Воробьев #:
по причине того, что так употребляется (по моим наблюдениям) наиболее часто

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

ЗЫ. Эксперимент с обезьянками помнишь, я лично его почти всегда в такие моменты вспоминаю.

Александр Воробьев
На сайте с 03.02.2020
Online
64
#547
ArbNet #:

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

ЗЫ. Эксперимент с обезьянками помнишь, я лично его почти всегда в такие моменты вспоминаю.

У меня есть более важные задачи для размышлений. В данном же случае ты хоть и очень активно "агитируешь" за одну из позиций не привел ни одного веского аргумента. Повторюсь: мы конечно можем обсудить и это, но предлагаю тогда в отдельную тему, если тебе интересен аргументированное обсуждение, а здесь лучше сосредоточиться на коде и решениях.
Александр Воробьев
На сайте с 03.02.2020
Online
64
#548
ArbNet #:
всё банально по методичкам..

Это, кстати, я могу аргументировать:

1. Любой человек в любой области накапливает опыт. В этом накопленном опыте ему что то нравится, что то нет и так далее. Переходя к разработке фреймоворков: вполне естественно, что имея за плечами опыт разработки в не учебных/академических проектах накапливаешь опыт: какие решения, идеи хорошие и удобные, какие нет. Соотвтственно и свое решение строишь с учетом опыта. Берешь на вооружение хорошее и удобное (и этом может быть алгоритмы, идеи, интерфейсы взаимодействия, те же стандарты, PSR и прочее и прочее) - делаешь свою реализацию, а что показалось плохим и не удобным переделываешь. Если опыта нет (особенно даже если учебного) - то тут будет все по максимуму свое. Но у меня нет цели делать из принципа "главное, чтоб не было похоже на что то существующее" (считаю такой принцип не разумным). по этому да у меня есть решения которые похожи на типовые.

2. Фреймоворк должен быть привычным. Особенно в той парадигме в которой задумал я: он должен подходить для максимального числа проектов (в т.ч. и для чайников - через создание на его базе конструктора сайтов или MCP сервера, помогающего чайнику объяснить словами свои хотелки ИИ, а тот ему сгенерит).    А если расчет и на разработчиков глупо их полностью переучивать. Фреймворк должен помогать выполнять основную задачу, а не загружать мозг дополнительными навыками.  При чем если делать исходя "главное чтоб не как у всех" придется и самому полностью переучиваться, а это не имеет смысла в тех моментах которые считаешь правильными.

Авторизуйтесь или зарегистрируйтесь, чтобы оставить комментарий