CVE-2021-41170 (published 2021-11-08T19:15Z)

neoan3-apps/template is a neoan3 minimal template engine. Versions prior to 1.1.1 have allowed for passing in closures directly into the template engine. As a result values that are callable are executed by the template engine. The issue arises if a value has the same name as a method or function in scope and can therefore be executed either by mistake or maliciously. In theory all users of the package are affected as long as they either deal with direct user input or database values. A multi-step attack on is therefore plausible. Version 1.1.1 has addressed this vulnerability. Unfortunately only working with hardcoded values is safe in prior versions. As this likely defeats the purpose of a template engine, please upgrade.

Severity: HIGH
CVSS v2 base score: 7.5 (AV:N/AC:L/Au:N/C:P/I:P/A:P)
CVSS v3 base score: 9.8, CRITICAL (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H)

Weakness information:
cwe_id: CWE-74
cwe_name: Improper Neutralization of Special Elements in Output Used by a Downstream Component ('Injection')
description: The product constructs all or part of a command, data structure, or record using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify how it is parsed or interpreted when it is sent to a downstream component.
extended_description: Software or other automated logic has certain assumptions about what constitutes data and control respectively. It is the lack of verification of these assumptions for user-controlled input that leads to injection problems. Injection problems encompass a wide variety of issues -- all mitigated in very different ways and usually attempted in order to alter the control flow of the process. For this reason, the most effective way to discuss these weaknesses is to note the distinct features that classify them as injection weaknesses. The most important issue to note is that all injection problems share one thing in common -- i.e., they allow for the injection of control plane data into the user-controlled data plane. This means that the execution of the process may be altered by sending code in through legitimate data channels, using no other mechanism. While buffer overflows, and many other flaws, involve the use of some further issue to gain execution, injection problems need only for the data to be parsed.
         
cwe_id: CWE-277
cwe_name: Insecure Inherited Permissions
description: A product defines a set of insecure permissions that are inherited by objects that are created by the program.
extended_description: 
            
         
         

Fix information:
repo_url: https://github.com/sroehrl/neoan3-template
commit_details: repo_url: https://github.com/sroehrl/neoan3-template
author: neoan
author_date: 2021-10-21 19:25:24-04:00
msg: SECURITY: allowing for direct injection (Issue #8)
repository: repo_url: https://github.com/sroehrl/neoan3-template
repo_name: sroehrl/neoan3-template
description: neoan3 minimal template engine
repo_language: PHP
owner: sroehrl
file_changes: filename: Template.php
change_type: ModificationType.MODIFY
diff: @@ -24,12 +24,10 @@ class Template
         $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;

programming_language: PHP
filename: TemplateFunctions.php
change_type: ModificationType.MODIFY
diff: @@ -103,7 +103,7 @@ class TemplateFunctions
         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 @@ class TemplateFunctions
         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 @@ class TemplateFunctions
             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 {

programming_language: PHP
filename: composer.json
change_type: ModificationType.MODIFY
diff: @@ -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": "*",

programming_language: JavaScript
filename: TemplateTest.php
change_type: ModificationType.MODIFY
diff: @@ -113,11 +113,11 @@ class TemplateTest extends TestCase
     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);

programming_language: PHP