Bladeren bron

Merge branch 'hotfix/2.2.0' into dev

Conflicts:
	src/Mustache/Compiler.php
	src/Mustache/Engine.php
Justin Hileman 12 jaren geleden
bovenliggende
commit
01d413de0d

+ 44 - 26
src/Mustache/Compiler.php

@@ -22,27 +22,30 @@ class Mustache_Compiler
     private $indentNextLine;
     private $customEscape;
     private $charset;
+    private $strictCallables;
     private $pragmas;
 
     /**
      * Compile a Mustache token parse tree into PHP source code.
      *
-     * @param string $source       Mustache Template source code
-     * @param string $tree         Parse tree of Mustache tokens
-     * @param string $name         Mustache Template class name
-     * @param bool   $customEscape (default: false)
-     * @param string $charset      (default: 'UTF-8')
+     * @param string $source          Mustache Template source code
+     * @param string $tree            Parse tree of Mustache tokens
+     * @param string $name            Mustache Template class name
+     * @param bool   $customEscape    (default: false)
+     * @param string $charset         (default: 'UTF-8')
+     * @param bool   $strictCallables (default: false)
      *
      * @return string Generated PHP source code
      */
-    public function compile($source, array $tree, $name, $customEscape = false, $charset = 'UTF-8')
+    public function compile($source, array $tree, $name, $customEscape = false, $charset = 'UTF-8', $strictCallables = false)
     {
-        $this->pragmas        = array();
-        $this->sections       = array();
-        $this->source         = $source;
-        $this->indentNextLine = true;
-        $this->customEscape   = $customEscape;
-        $this->charset        = $charset;
+        $this->pragmas         = array();
+        $this->sections        = array();
+        $this->source          = $source;
+        $this->indentNextLine  = true;
+        $this->customEscape    = $customEscape;
+        $this->charset         = $charset;
+        $this->strictCallables = $strictCallables;
 
         return $this->writeCode($tree, $name);
     }
@@ -124,7 +127,7 @@ class Mustache_Compiler
 
         class %s extends Mustache_Template
         {
-            private $lambdaHelper;
+            private $lambdaHelper;%s
 
             public function renderInternal(Mustache_Context $context, $indent = \'\')
             {
@@ -140,7 +143,7 @@ class Mustache_Compiler
     const KLASS_NO_LAMBDAS = '<?php
 
         class %s extends Mustache_Template
-        {
+        {%s
             public function renderInternal(Mustache_Context $context, $indent = \'\')
             {
                 $buffer = \'\';
@@ -150,6 +153,8 @@ class Mustache_Compiler
             }
         }';
 
+    const STRICT_CALLABLE = 'protected $strictCallables = true;';
+
     /**
      * Generate Mustache Template class PHP source.
      *
@@ -163,8 +168,9 @@ class Mustache_Compiler
         $code     = $this->walk($tree);
         $sections = implode("\n", $this->sections);
         $klass    = empty($this->sections) ? self::KLASS_NO_LAMBDAS : self::KLASS;
+        $callable = $this->strictCallables ? $this->prepare(self::STRICT_CALLABLE) : '';
 
-        return sprintf($this->prepare($klass, 0, false, true), $name, $code, $sections);
+        return sprintf($this->prepare($klass, 0, false, true), $name, $callable, $code, $sections);
     }
 
     const SECTION_CALL = '
@@ -176,7 +182,7 @@ class Mustache_Compiler
         private function section%s(Mustache_Context $context, $indent, $value)
         {
             $buffer = \'\';
-            if (!is_string($value) && is_callable($value)) {
+            if (%s) {
                 $source = %s;
                 $buffer .= $this->mustache
                     ->loadLambda((string) call_user_func($value, $source, $this->lambdaHelper)%s)
@@ -207,9 +213,10 @@ class Mustache_Compiler
      */
     private function section($nodes, $id, $start, $end, $otag, $ctag, $level)
     {
-        $method = $this->getFindMethod($id);
-        $id     = var_export($id, true);
-        $source = var_export(substr($this->source, $start, $end - $start), true);
+        $method   = $this->getFindMethod($id);
+        $id       = var_export($id, true);
+        $source   = var_export(substr($this->source, $start, $end - $start), true);
+        $callable = $this->getCallable();
 
         if ($otag !== '{{' || $ctag !== '}}') {
             $delims = ', '.var_export(sprintf('{{= %s %s =}}', $otag, $ctag), true);
@@ -220,7 +227,7 @@ class Mustache_Compiler
         $key    = ucfirst(md5($delims."\n".$source));
 
         if (!isset($this->sections[$key])) {
-            $this->sections[$key] = sprintf($this->prepare(self::SECTION), $key, $source, $delims, $this->walk($nodes, 2));
+            $this->sections[$key] = sprintf($this->prepare(self::SECTION), $key, $callable, $source, $delims, $this->walk($nodes, 2));
         }
 
         return sprintf($this->prepare(self::SECTION_CALL, $level), $id, $key, $method, $id);
@@ -321,7 +328,7 @@ class Mustache_Compiler
 
     const FILTER = '
         $filter = $context->%s(%s);
-        if (is_string($filter) || !is_callable($filter)) {
+        if (!(%s)) {
             throw new Mustache_Exception_UnknownFilterException(%s);
         }
         $value = call_user_func($filter, $value);%s
@@ -341,12 +348,13 @@ class Mustache_Compiler
             return '';
         }
 
-        $name   = array_shift($filters);
-        $method = $this->getFindMethod($name);
-        $filter = ($method !== 'last') ? var_export($name, true) : '';
-        $msg    = var_export($name, true);
+        $name     = array_shift($filters);
+        $method   = $this->getFindMethod($name);
+        $filter   = ($method !== 'last') ? var_export($name, true) : '';
+        $callable = $this->getCallable('$filter');
+        $msg      = var_export($name, true);
 
-        return sprintf($this->prepare(self::FILTER, $level), $method, $filter, $msg, $this->getFilter($filters, $level));
+        return sprintf($this->prepare(self::FILTER, $level), $method, $filter, $callable, $msg, $this->getFilter($filters, $level));
     }
 
     const LINE = '$buffer .= "\n";';
@@ -437,6 +445,16 @@ class Mustache_Compiler
         }
     }
 
+    const IS_CALLABLE        = '!is_string(%s) && is_callable(%s)';
+    const STRICT_IS_CALLABLE = 'is_object(%s) && is_callable(%s)';
+
+    private function getCallable($variable = '$value')
+    {
+        $tpl = $this->strictCallables ? self::STRICT_IS_CALLABLE : self::IS_CALLABLE;
+
+        return sprintf($tpl, $variable, $variable);
+    }
+
     const LINE_INDENT = '$indent . ';
 
     /**

+ 16 - 3
src/Mustache/Engine.php

@@ -23,7 +23,7 @@
  */
 class Mustache_Engine
 {
-    const VERSION        = '2.1.0';
+    const VERSION        = '2.2.0';
     const SPEC_VERSION   = '1.1.2';
 
     const PRAGMA_FILTERS = 'FILTERS';
@@ -41,6 +41,7 @@ class Mustache_Engine
     private $escape;
     private $charset = 'UTF-8';
     private $logger;
+    private $strictCallables = false;
 
     /**
      * Mustache class constructor.
@@ -87,6 +88,13 @@ class Mustache_Engine
      *         // logging library -- such as Monolog -- is highly recommended. A simple stream logger implementation is
      *         // available as well:
      *         'logger' => new Mustache_Logger_StreamLogger('php://stderr'),
+     *
+     *         // Only treat Closure instances and invokable classes as callable. If true, values like
+     *         // `array('ClassName', 'methodName')` and `array($classInstance, 'methodName')`, which are traditionally
+     *         // "callable" in PHP, are not called to resolve variables for interpolation or section contexts. This
+     *         // helps protect against arbitrary code execution when user input is passed directly into the template.
+     *         // This currently defaults to false, but will default to true in v3.0.
+     *         'strict_callables' => true,
      *     );
      *
      * @throws Mustache_Exception_InvalidArgumentException If `escape` option is not callable.
@@ -138,6 +146,10 @@ class Mustache_Engine
         if (isset($options['logger'])) {
             $this->setLogger($options['logger']);
         }
+
+        if (isset($options['strict_callables'])) {
+            $this->strictCallables = $options['strict_callables'];
+        }
     }
 
     /**
@@ -459,10 +471,11 @@ class Mustache_Engine
     public function getTemplateClassName($source)
     {
         return $this->templateClassPrefix . md5(sprintf(
-            'version:%s,escape:%s,charset:%s,source:%s',
+            'version:%s,escape:%s,charset:%s,strict_callables:%s,source:%s',
             self::VERSION,
             isset($this->escape) ? 'custom' : 'default',
             $this->charset,
+            $this->strictCallables ? 'true' : 'false',
             $source
         ));
     }
@@ -631,7 +644,7 @@ class Mustache_Engine
             array('className' => $name)
         );
 
-        return $this->getCompiler()->compile($source, $tree, $name, isset($this->escape), $this->charset);
+        return $this->getCompiler()->compile($source, $tree, $name, isset($this->escape), $this->charset, $this->strictCallables);
     }
 
     /**

+ 6 - 1
src/Mustache/Template.php

@@ -22,6 +22,11 @@ abstract class Mustache_Template
      */
     protected $mustache;
 
+    /**
+     * @var boolean
+     */
+    protected $strictCallables = false;
+
     /**
      * Mustache Template constructor.
      *
@@ -161,7 +166,7 @@ abstract class Mustache_Template
      */
     protected function resolveValue($value, Mustache_Context $context, $indent = '')
     {
-        if (!is_string($value) && is_callable($value)) {
+        if (($this->strictCallables ? is_object($value) : !is_string($value)) && is_callable($value)) {
             return $this->mustache
                 ->loadLambda((string) call_user_func($value))
                 ->renderInternal($context, $indent);

+ 147 - 0
test/Mustache/Test/FiveThree/Functional/StrictCallablesTest.php

@@ -0,0 +1,147 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2013 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * @group lambdas
+ * @group functional
+ */
+class Mustache_Test_FiveThree_Functional_StrictCallablesTest extends PHPUnit_Framework_TestCase
+{
+    /**
+     * @dataProvider callables
+     */
+    public function testStrictCallablesDisabled($name, $section, $expected)
+    {
+        $mustache = new Mustache_Engine(array('strict_callables' => false));
+        $tpl      = $mustache->loadTemplate('{{# section }}{{ name }}{{/ section }}');
+
+        $data = new StdClass;
+        $data->name    = $name;
+        $data->section = $section;
+
+        $this->assertEquals($expected, $tpl->render($data));
+    }
+
+    public function callables()
+    {
+        $lambda = function($tpl, $mustache) {
+            return strtoupper($mustache->render($tpl));
+        };
+
+        return array(
+            // Interpolation lambdas
+            array(
+                array($this, 'instanceName'),
+                $lambda,
+                'YOSHI',
+            ),
+            array(
+                array(__CLASS__, 'staticName'),
+                $lambda,
+                'YOSHI',
+            ),
+            array(
+                function() { return 'Yoshi'; },
+                $lambda,
+                'YOSHI',
+            ),
+
+            // Section lambdas
+            array(
+                'Yoshi',
+                array($this, 'instanceCallable'),
+                'YOSHI',
+            ),
+            array(
+                'Yoshi',
+                array(__CLASS__, 'staticCallable'),
+                'YOSHI',
+            ),
+            array(
+                'Yoshi',
+                $lambda,
+                'YOSHI',
+            ),
+        );
+    }
+
+
+    /**
+     * @group wip
+     * @dataProvider strictCallables
+     */
+    public function testStrictCallablesEnabled($name, $section, $expected)
+    {
+        $mustache = new Mustache_Engine(array('strict_callables' => true));
+        $tpl      = $mustache->loadTemplate('{{# section }}{{ name }}{{/ section }}');
+
+        $data = new StdClass;
+        $data->name    = $name;
+        $data->section = $section;
+
+        $this->assertEquals($expected, $tpl->render($data));
+    }
+
+    public function strictCallables()
+    {
+        $lambda = function($tpl, $mustache) {
+            return strtoupper($mustache->render($tpl));
+        };
+
+        return array(
+            // Interpolation lambdas
+            array(
+                function() { return 'Yoshi'; },
+                $lambda,
+                'YOSHI',
+            ),
+
+            // Section lambdas
+            array(
+                'Yoshi',
+                array($this, 'instanceCallable'),
+                'YoshiYoshi',
+            ),
+            array(
+                'Yoshi',
+                array(__CLASS__, 'staticCallable'),
+                'YoshiYoshi',
+            ),
+            array(
+                'Yoshi',
+                function($tpl, $mustache) {
+                    return strtoupper($mustache->render($tpl));
+                },
+                'YOSHI',
+            ),
+        );
+    }
+
+    public function instanceCallable($tpl, $mustache)
+    {
+        return strtoupper($mustache->render($tpl));
+    }
+
+    public static function staticCallable($tpl, $mustache)
+    {
+        return strtoupper($mustache->render($tpl));
+    }
+
+    public function instanceName()
+    {
+        return 'Yoshi';
+    }
+
+    public static function staticName()
+    {
+        return 'Yoshi';
+    }
+}