Evaluation of closures can lead to execution of methods & functions in current program scope
Description
neoan3-template before 1.1.1 allows direct closure injection, causing unintended execution of callable values from user input.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
neoan3-template before 1.1.1 allows direct closure injection, causing unintended execution of callable values from user input.
Vulnerability
Neoan3-apps/template (neoan3 minimal template engine) versions prior to 1.1.1 allow passing closures directly into the template engine. The vulnerability occurs when a value in the substitution array has the same name as a callable function or method in the current scope, causing unintended execution of that callable during template rendering. The issue was present in the embrace and related methods, affecting all users handling dynamic input [1][4].
Exploitation
An attacker needs the ability to supply data to the template engine, either through direct user input or by controlling database values. By providing a key named after an existing function or method (e.g., strtolower), the attacker can cause that function to be executed with the value as argument. A multi-step attack is plausible where the attacker first registers a closure via registerClosure or manipulates the substitution array to trigger unintended calls [1][4]. The commit patch shows that closures are no longer directly accepted [3].
Impact
Successful exploitation allows an attacker to execute arbitrary functions or methods in the context of the application. This can lead to information disclosure, code execution, or other malicious outcomes depending on the available functions and the application's environment. The attacker gains the same privileges as the template engine's runtime [1][4].
Mitigation
The vulnerability is fixed in version 1.1.1, released on 2021-11-08. Users should upgrade to this version or later. There are no workarounds other than hardcoding all template values, which defeats the purpose of a template engine. The patch disallows direct passing of closures in the substitution array, while still allowing registered closures via TemplateFunctions::registerClosure [3][4].
AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
neoan3-apps/templatePackagist | < 1.1.1 | 1.1.1 |
Affected products
2- Range: < 1.1.1
Patches
14a2c9570f071SECURITY: allowing for direct injection (Issue #8)
4 files changed · +12 −15
composer.json+1 −2 modified@@ -1,13 +1,12 @@ { "name": "neoan3-apps/template", "description": "neoan3 minimal template engine", - "version": "1.1.0", + "version": "1.1.1", "license": "MIT", "autoload": { "psr-4": { "Neoan3\\Apps\\": "./" } - }, "require": { "ext-openssl": "*",
TemplateFunctions.php+4 −4 modified@@ -103,7 +103,7 @@ private static function retrieveClosurePattern($pure, $closureName) if (!$pure) { $pattern .= preg_quote(self::$registeredDelimiters[0]) . "\s*"; } - $pattern .= "$closureName\(([a-z0-9,\.\s]+)\)"; + $pattern .= "$closureName\(([a-z0-9,\.\s_]+)\)"; if (!$pure) { $pattern .= "\s*" . preg_quote(self::$registeredDelimiters[1]); } @@ -201,7 +201,6 @@ private static function evaluateTypedCondition(array $flatArray, $expression): b foreach ($flatArray as $key => $value) { $pattern = '/' . $key . '([^.]|$)/'; if (preg_match($pattern, $expression, $matches)) { - switch (gettype($flatArray[$key])) { case 'boolean': $expression = str_replace($key, $flatArray[$key] ? 'true' : 'false', $expression); @@ -241,11 +240,12 @@ static function nIf($content, $array) return $content; } + $array = Template::flattenArray($array); + // important: first try closures + $array = array_merge(self::$registeredClosures, $array); foreach ($hits as $hit) { $expression = $hit->getAttribute('n-if'); - $array = Template::flattenArray($array); $bool = self::evaluateTypedCondition($array, $expression); - if (!$bool) { $hit->parentNode->removeChild($hit); } else {
Template.php+4 −6 modified@@ -24,12 +24,10 @@ static function embrace($content, $array) $saveClosing = preg_quote(TemplateFunctions::getDelimiters()[1]); foreach ($flatArray as $flatKey => $value){ $flatKey = preg_replace('/[\/\.\\\+\*\?\[\^\]\$\(\)\{\}\=\!\<\>\|\:\-]/', "\\\\$0",$flatKey); - if(is_callable($value)){ - TemplateFunctions::registerClosure($flatKey,$value); - } else { - $content = preg_replace("/$saveOpening\s*$flatKey\s*$saveClosing/", $value, $content); - $content = TemplateFunctions::tryClosures($flatArray, $content, false); - } + // PATCHED: direct function injection is not allowed anymore + $content = preg_replace("/$saveOpening\s*$flatKey\s*$saveClosing/", $value, $content); + $content = TemplateFunctions::tryClosures($flatArray, $content, false); + } return $content;
tests/TemplateTest.php+3 −3 modified@@ -113,11 +113,11 @@ public function testEmbraceTypes() public function testCallback() { $array = [ - 'myFunc' => function ($x) { - return strtoupper($x); - }, 'some' => 'value' ]; + TemplateFunctions::registerClosure('myFunc',function($x){ + return strtoupper($x); + }); $t = Template::embraceFromFile('callback.html', $array); $this->assertStringContainsString('<p>VALUE</p>', $t);
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-3v56-q6r6-4gcwghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2021-41170ghsaADVISORY
- github.com/sroehrl/neoan3-template/commit/4a2c9570f071d3c8f4ac790007599cba20e16934ghsax_refsource_MISCWEB
- github.com/sroehrl/neoan3-template/issues/8ghsax_refsource_MISCWEB
- github.com/sroehrl/neoan3-template/security/advisories/GHSA-3v56-q6r6-4gcwghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.