CVE-2022-39365 (published 2022-10-27T15:15Z)

Pimcore is an open source data and experience management platform. Prior to version 10.5.9, the user controlled twig templates rendering in `Pimcore/Mail` & `ClassDefinition\\Layout\\Text` is vulnerable to server-side template injection, which could lead to remote code execution. Version 10.5.9 contains a patch for this issue. As a workaround, one may apply the patch manually.

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-94
cwe_name: Improper Control of Generation of Code ('Code Injection')
description: The product constructs all or part of a code segment using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the syntax or behavior of the intended code segment.
extended_description: 
            When a product allows a user's input to contain code syntax, it might be possible for an attacker to craft the code in such a way that it will alter the intended control flow of the product. Such an alteration could lead to arbitrary code execution.
            Injection problems encompass a wide variety of issues -- all mitigated in very different ways. For this reason, the most effective way to discuss these weaknesses is to note the distinct features which 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. The most classic instantiations of this category of weakness are SQL injection and format string vulnerabilities.
         
         

Fix information:
repo_url: https://github.com/pimcore/pimcore
commit_details: repo_url: https://github.com/pimcore/pimcore
author: Divesh Pahuja
author_date: 2022-10-27 15:09:23+02:00
msg: [Mail] Renderer email content twig templates in a sandbox (#13347)

* [Mail] Renderer email content twig templates in a sandbox

* [Mail] Renderer email content twig templates in a sandbox

* [Mail] Renderer email content twig templates in a sandbox

* [Mail] Renderer email content twig templates in a sandbox

* [Mail] Renderer email content twig templates in a sandbox

* [Mail] Renderer email content twig templates in a sandbox

* Apply suggestions from code review

Co-authored-by: Sebastian Blank <blank@data-factory.net>

* Update lib/Templating/TwigDefaultDelegatingEngine.php

Co-authored-by: Jacob Dreesen <j.dreesen@neusta.de>

* [Twig] Renderer user controlled twig templates in a sandbox - review changes #13347

* [Twig] Renderer user controlled twig templates in a sandbox - use custom security policy to whitelist object properties and methods execution by default #13347

* [Twig] Renderer user controlled twig templates in a sandbox - review changes #13347

* [Twig] Renderer user controlled twig templates in a sandbox - fix phpstan #13347

* [Twig] Renderer user controlled twig templates in a sandbox - fix service definition #13347

* [Twig] Renderer user controlled twig templates in a sandbox - docs typo #13347

Co-authored-by: Sebastian Blank <blank@data-factory.net>
Co-authored-by: Jacob Dreesen <j.dreesen@neusta.de>
repository: repo_url: https://github.com/pimcore/pimcore
repo_name: pimcore/pimcore
description: Core Framework for the Open Source Data & Experience Management Platform (PIM, MDM, CDP, DAM, DXP/CMS & Digital Commerce)
repo_language: PHP
owner: pimcore
file_changes: filename: Configuration.php
change_type: ModificationType.MODIFY
diff: @@ -180,6 +180,7 @@ final class Configuration implements ConfigurationInterface
         $this->addCustomViewsNode($rootNode);
         $this->addGlossaryNode($rootNode);
         $this->buildRedirectsStatusCodes($rootNode);
+        $this->addTemplatingEngineNode($rootNode);
 
         return $treeBuilder;
     }
@@ -2276,4 +2277,34 @@ final class Configuration implements ConfigurationInterface
                             ->useAttributeAsKey('name')
                             ->prototype('scalar');
     }
+
+    private function addTemplatingEngineNode(ArrayNodeDefinition $rootNode): void
+    {
+        $rootNode
+            ->children()
+            ->arrayNode('templating_engine')
+            ->addDefaultsIfNotSet()
+            ->children()
+                ->arrayNode('twig')
+                ->addDefaultsIfNotSet()
+                ->children()
+                    ->arrayNode('sandbox_security_policy')
+                        ->info('Whitelist tags, filters & functions for evaluating twig 
+                        templates in a sandbox environment e.g. used by Mailer & Text layout component.')
+                        ->children()
+                            ->arrayNode('tags')
+                                ->scalarPrototype()->end()
+                            ->end()
+                            ->arrayNode('filters')
+                                ->scalarPrototype()->end()
+                            ->end()
+                            ->arrayNode('functions')
+                                ->scalarPrototype()->end()
+                            ->end()
+                        ->end()
+                    ->end()
+                ->end()
+            ->end()
+        ->end();
+    }
 }

programming_language: PHP
filename: PimcoreCoreExtension.php
change_type: ModificationType.MODIFY
diff: @@ -95,6 +95,11 @@ final class PimcoreCoreExtension extends ConfigurableExtension implements Prepen
         $container->setParameter('pimcore.documents.web_to_print.default_controller_print_page', $config['documents']['web_to_print']['default_controller_print_page']);
         $container->setParameter('pimcore.documents.web_to_print.default_controller_print_container', $config['documents']['web_to_print']['default_controller_print_container']);
 
+        //twig security policy whitelist config
+        $container->setParameter('pimcore.templating.twig.sandbox_security_policy.tags', $config['templating_engine']['twig']['sandbox_security_policy']['tags']);
+        $container->setParameter('pimcore.templating.twig.sandbox_security_policy.filters', $config['templating_engine']['twig']['sandbox_security_policy']['filters']);
+        $container->setParameter('pimcore.templating.twig.sandbox_security_policy.functions', $config['templating_engine']['twig']['sandbox_security_policy']['functions']);
+
         // register pimcore config on container
         // TODO is this bad practice?
         // TODO only extract what we need as parameter?

programming_language: PHP
filename: default.yaml
change_type: ModificationType.MODIFY
diff: @@ -293,6 +293,12 @@ pimcore:
               'abbr', 'option', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'
           ]
 
+    templating_engine:
+        twig:
+            sandbox_security_policy:
+                tags: ['set']
+                filters: ['escape', 'trans']
+                functions: ['path', 'asset']
 presta_sitemap:
     # do not add properties by default
     defaults:

programming_language: PHP
filename: templating_twig.yaml
change_type: ModificationType.MODIFY
diff: @@ -84,4 +84,15 @@ services:
 
     Twig\Extra\String\StringExtension: ~
 
+    Pimcore\Twig\Sandbox\SecurityPolicy:
+        arguments:
+            $allowedTags: '%pimcore.templating.twig.sandbox_security_policy.tags%'
+            $allowedFilters: '%pimcore.templating.twig.sandbox_security_policy.filters%'
+            $allowedFunctions: '%pimcore.templating.twig.sandbox_security_policy.functions%'
+
+    Twig\Extension\SandboxExtension:
+        class: Twig\Extension\SandboxExtension
+        arguments:
+            $policy: '@Pimcore\Twig\Sandbox\SecurityPolicy'
+
     Pimcore\Twig\Extension\AdminExtension: ~

programming_language: PHP
filename: 01_Dynamic_Text_Labels.md
change_type: ModificationType.MODIFY
diff: @@ -67,3 +67,17 @@ Here is an example of Twig content in htmleditor source edit mode:
 ![Template Class Definition](../../../img/dynamic_textlabel_3.png)
 
 ![Template editmode](../../../img/dynamic_textlabel_4.png)
+
+### Sandbox Restrictions
+Dynamic Text renders user controlled twig templates in a sandbox with restrictive
+security policies for tags, filters & functions. Please use following configuration to allow more in template rendering:
+
+```yaml
+    pimcore:
+          templating_engine:
+              twig:
+                sandbox_security_policy:
+                  tags: ['if']
+                  filters: ['upper']
+                  functions: ['include', 'path']
+```
\ No newline at end of file

programming_language: Markdown
filename: README.md
change_type: ModificationType.MODIFY
diff: @@ -98,3 +98,17 @@ $mail->bcc("bcc@pimcore.org");
 $mail->html("<b>some</b> rich text");
 $mail->send();
 ```
+
+## Sandbox Restrictions
+Sending mails renders user controlled twig templates in a sandbox with restrictive 
+security policies for tags, filters & functions. Please use following configuration to allow more in template rendering:
+
+```yaml
+    pimcore:
+          templating_engine:
+              twig:
+                sandbox_security_policy:
+                  tags: ['if']
+                  filters: ['upper']
+                  functions: ['include', 'path']
+```
\ No newline at end of file

programming_language: Markdown
filename: README.md
change_type: ModificationType.MODIFY
diff: @@ -1,6 +1,18 @@
 # Upgrade Notes
 
 ## 10.5.8
+- [Twig] Sending mails and Dataobject Text Layouts, which allow rendering user controlled twig templates are now executed in a sandbox with restrictive security policies for tags, filters, functions.
+         Please use following configuration to allow more in template rendering:
+  ```yaml
+  pimcore:
+        templating_engine:
+            twig:
+              sandbox_security_policy:
+                tags: ['if']
+                filters: ['upper']
+                functions: ['include', 'path', 'range']
+  ```
+
 - [Nginx] Static pages nginx config has been updated to fix the issue for home static page generation. please adapt the following configuration:
 ```nginx
 map $args $static_page_root {

programming_language: Markdown
filename: Mail.php
change_type: ModificationType.MODIFY
diff: @@ -28,6 +28,7 @@ use Symfony\Component\Mime\Email;
 use Symfony\Component\Mime\Header\Headers;
 use Symfony\Component\Mime\Header\MailboxListHeader;
 use Symfony\Component\Mime\Part\AbstractPart;
+use Twig\Sandbox\SecurityError;
 
 class Mail extends Email
 {
@@ -651,13 +652,22 @@ class Mail extends Email
      *
      * @return string
      */
-    private function renderParams(string $string): string
+    private function renderParams(string $string, string $context): string
     {
         $templatingEngine = \Pimcore::getContainer()->get('pimcore.templating.engine.delegating');
-        $twig = $templatingEngine->getTwigEnvironment();
-        $template = $twig->createTemplate($string);
-
-        return $template->render($this->getParams());
+        try {
+            $twig = $templatingEngine->getTwigEnvironment(true);
+            $template = $twig->createTemplate($string);
+
+            return $template->render($this->getParams());
+        } catch (SecurityError $e) {
+            Logger::err((string) $e);
+
+            throw new \Exception(sprintf("Failed rendering the %s: %s. Please check your twig sandbox security policy or contact the administrator.",
+            $context, substr($e->getMessage(),0, strpos($e->getMessage(), ' in "__string'))));
+        } finally {
+            $templatingEngine->disableSandboxExtensionFromTwigEnvironment();
+        }
     }
 
     /**
@@ -676,7 +686,7 @@ class Mail extends Email
         }
 
         if ($subject) {
-            return $this->renderParams($subject);
+            return $this->renderParams($subject, 'subject');
         }
 
         return '';
@@ -707,7 +717,7 @@ class Mail extends Email
 
         $content = null;
         if ($html) {
-            $content = $this->renderParams($html);
+            $content = $this->renderParams($html, 'body');
 
             // modifying the content e.g set absolute urls...
             $content = MailHelper::embedAndModifyCss($content, $this->getDocument());
@@ -731,7 +741,7 @@ class Mail extends Email
 
         //if the content was manually set with $obj->text(); this content will be used
         if ($text) {
-            $content = $this->renderParams($text);
+            $content = $this->renderParams($text, 'body');
         } else {
             //creating text version from html email
             try {

programming_language: PHP
filename: TwigDefaultDelegatingEngine.php
change_type: ModificationType.MODIFY
diff: @@ -15,34 +15,28 @@
 
 namespace Pimcore\Templating;
 
+use Pimcore\Config;
 use Symfony\Component\HttpFoundation\Response;
 use Symfony\Component\Templating\DelegatingEngine as BaseDelegatingEngine;
 use Symfony\Component\Templating\EngineInterface;
 use Twig\Environment;
+use Twig\Extension\SandboxExtension;
 
 /**
  * @internal
  */
 class TwigDefaultDelegatingEngine extends BaseDelegatingEngine
 {
-    /**
-     * @var Environment
-     */
-    protected $twig;
-
     /**
      * @var bool
      */
     protected $delegate = false;
 
     /**
-     * @param Environment $twig
      * @param EngineInterface[] $engines
      */
-    public function __construct(Environment $twig, array $engines = [])
+    public function __construct(protected Environment $twig, protected Config $config, array $engines = [])
     {
-        $this->twig = $twig;
-
         parent::__construct($engines);
     }
 
@@ -100,14 +94,24 @@ class TwigDefaultDelegatingEngine extends BaseDelegatingEngine
         return $this->delegate;
     }
 
-    /**
-     * @return Environment
-     */
-    public function getTwigEnvironment(): Environment
+    public function getTwigEnvironment(bool $sandboxed = false): Environment
     {
+        if ($sandboxed) {
+            /** @var SandboxExtension $sandboxExtension */
+            $sandboxExtension = $this->twig->getExtension(SandboxExtension::class);
+            $sandboxExtension->enableSandbox();
+        }
+
         return $this->twig;
     }
 
+    public function disableSandboxExtensionFromTwigEnvironment(): void
+    {
+        /** @var SandboxExtension $sandboxExtension */
+        $sandboxExtension = $this->twig->getExtension(SandboxExtension::class);
+        $sandboxExtension->disableSandbox();
+    }
+
     /**
      * @param string $view
      * @param array $parameters

programming_language: PHP
filename: SecurityPolicy.php
change_type: ModificationType.ADD
diff: @@ -0,0 +1,91 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * Pimcore
+ *
+ * This source file is available under two different licenses:
+ * - GNU General Public License version 3 (GPLv3)
+ * - Pimcore Commercial License (PCL)
+ * Full copyright and license information is available in
+ * LICENSE.md which is distributed with this source code.
+ *
+ *  @copyright  Copyright (c) Pimcore GmbH (http://www.pimcore.org)
+ *  @license    http://www.pimcore.org/license     GPLv3 and PCL
+ */
+
+namespace Pimcore\Twig\Sandbox;
+
+use Twig\Sandbox\SecurityNotAllowedFilterError;
+use Twig\Sandbox\SecurityNotAllowedFunctionError;
+use Twig\Sandbox\SecurityNotAllowedTagError;
+use Twig\Sandbox\SecurityPolicyInterface;
+
+/**
+ * Note: Reused to disable checks on object methods and properties.
+ *
+ * Represents a security policy which need to be enforced when sandbox mode is enabled.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ */
+final class SecurityPolicy implements SecurityPolicyInterface
+{
+    private array $allowedTags;
+    private array $allowedFilters;
+    private array $allowedFunctions;
+
+    public function __construct(array $allowedTags = [], array $allowedFilters = [], array $allowedFunctions = [])
+    {
+        $this->allowedTags = $allowedTags;
+        $this->allowedFilters = $allowedFilters;
+        $this->allowedFunctions = $allowedFunctions;
+    }
+
+    public function setAllowedTags(array $tags)
+    {
+        $this->allowedTags = $tags;
+    }
+
+    public function setAllowedFilters(array $filters)
+    {
+        $this->allowedFilters = $filters;
+    }
+
+    public function setAllowedFunctions(array $functions)
+    {
+        $this->allowedFunctions = $functions;
+    }
+
+    public function checkSecurity($tags, $filters, $functions): void
+    {
+        foreach ($tags as $tag) {
+            if (!\in_array($tag, $this->allowedTags)) {
+                throw new SecurityNotAllowedTagError(sprintf('Tag "%s" is not allowed.', $tag), $tag);
+            }
+        }
+
+        foreach ($filters as $filter) {
+            if (!\in_array($filter, $this->allowedFilters)) {
+                throw new SecurityNotAllowedFilterError(sprintf('Filter "%s" is not allowed.', $filter), $filter);
+            }
+        }
+
+        foreach ($functions as $function) {
+            //check if a function is allowed or a pimcore twig functions
+            if (!\in_array($function, $this->allowedFunctions) && !str_starts_with($function, 'pimcore_')) {
+                throw new SecurityNotAllowedFunctionError(sprintf('Function "%s" is not allowed.', $function), $function);
+            }
+        }
+    }
+
+    public function checkMethodAllowed($obj, $method): void
+    {
+        //do not perform any checks
+    }
+
+    public function checkPropertyAllowed($obj, $method): void
+    {
+        //do not perform any checks
+    }
+}

programming_language: PHP
filename: Text.php
change_type: ModificationType.MODIFY
diff: @@ -15,8 +15,10 @@
 
 namespace Pimcore\Model\DataObject\ClassDefinition\Layout;
 
+use Pimcore\Logger;
 use Pimcore\Model;
 use Pimcore\Model\DataObject\Concrete;
+use Twig\Sandbox\SecurityError;
 
 class Text extends Model\DataObject\ClassDefinition\Layout implements Model\DataObject\ClassDefinition\Data\LayoutDefinitionEnrichmentInterface
 {
@@ -143,13 +145,23 @@ class Text extends Model\DataObject\ClassDefinition\Layout implements Model\Data
         }
 
         $templatingEngine = \Pimcore::getContainer()->get('pimcore.templating.engine.delegating');
-        $twig = $templatingEngine->getTwigEnvironment();
-        $template = $twig->createTemplate($this->html);
-        $this->html = $template->render(array_merge($context,
-            [
-                'object' => $object,
-            ]
-        ));
+        try {
+            $twig = $templatingEngine->getTwigEnvironment(true);
+            $template = $twig->createTemplate($this->html);
+            $this->html = $template->render(array_merge($context,
+                [
+                    'object' => $object,
+                ]
+            ));
+        } catch (SecurityError $e) {
+            Logger::err((string) $e);
+
+            $this->html = sprintf("<h2>Error</h2>Failed rendering the template: <b>%s</b>.
+                Please check your twig sandbox security policy or contact the administrator.",
+                substr($e->getMessage(),0, strpos($e->getMessage(), ' in "__string')));
+        } finally {
+            $templatingEngine->disableSandboxExtensionFromTwigEnvironment();
+        }
 
         return $this;
     }

programming_language: PHP