CVE-2020-15247: Other (NVD-CWE-Other)
value: October is a free, open-source, self-hosted CMS platform based on the Laravel PHP Framework. In October CMS from version 1.0.319 and before version 1.0.469, an authenticated backend user with the cms.manage_pages, cms.manage_layouts, or cms.manage_partials permissions who would normally not be permitted to provide PHP code to be executed by the CMS due to cms.enableSafeMode being enabled is able to write specific Twig code to escape the Twig sandbox and execute arbitrary PHP. This is not a problem for anyone that trusts their users with those permissions to normally write & manage PHP within the CMS by not having cms.enableSafeMode enabled, but would be a problem for anyone relying on cms.enableSafeMode to ensure that users with those permissions in production do not have access to write & execute arbitrary PHP. Issue has been patched in Build 469 (v1.0.469) and v1.1.0.
CWE description: NVD is only using a subset of CWE for mapping instead of the entire CWE, and the weakness type is not covered by that subset.
CVSS v3 base score: 5.2
Published: 2020-11-23T20:15Z
Repository: https://github.com/octobercms/october
Language: PHP
Files changed: modules/cms/classes/Controller.php, modules/system/ServiceProvider.php, modules/system/twig/SecurityPolicy.php
Commit date: 2020-09-04 13:02:01 +1000
Commit message: Security fixes for v1.0.469

Introduces sandbox policy to block extendable methods allowing arbitrary code execution

Diff: diff --git a/modules/cms/classes/Controller.php b/modules/cms/classes/Controller.php
index 381aab2bf..c86919f72 100644
--- a/modules/cms/classes/Controller.php
+++ b/modules/cms/classes/Controller.php
@@ -13,18 +13,20 @@ use Response;
 use Exception;
 use SystemException;
 use BackendAuth;
 use Twig\Environment as TwigEnvironment;
 use Twig\Cache\FilesystemCache as TwigCacheFilesystem;
+use Twig\Extension\SandboxExtension;
 use Cms\Twig\Loader as TwigLoader;
 use Cms\Twig\DebugExtension;
 use Cms\Twig\Extension as CmsTwigExtension;
 use Cms\Models\MaintenanceSetting;
 use System\Models\RequestLog;
 use System\Helpers\View as ViewHelper;
 use System\Classes\CombineAssets;
 use System\Twig\Extension as SystemTwigExtension;
+use System\Twig\SecurityPolicy;
 use October\Rain\Exception\AjaxException;
 use October\Rain\Exception\ValidationException;
 use October\Rain\Parse\Bracket as TextParser;
 use Illuminate\Http\RedirectResponse;
 
@@ -606,10 +608,11 @@ class Controller
         }
 
         $this->twig = new TwigEnvironment($this->loader, $options);
         $this->twig->addExtension(new CmsTwigExtension($this));
         $this->twig->addExtension(new SystemTwigExtension);
+        $this->twig->addExtension(new SandboxExtension(new SecurityPolicy, true));
 
         if ($isDebugMode) {
             $this->twig->addExtension(new DebugExtension($this));
         }
     }
diff --git a/modules/system/ServiceProvider.php b/modules/system/ServiceProvider.php
index 9fc858f39..055d89ba5 100644
--- a/modules/system/ServiceProvider.php
+++ b/modules/system/ServiceProvider.php
@@ -17,18 +17,20 @@ use System\Classes\PluginManager;
 use System\Classes\SettingsManager;
 use System\Classes\UpdateManager;
 use System\Twig\Engine as TwigEngine;
 use System\Twig\Loader as TwigLoader;
 use System\Twig\Extension as TwigExtension;
+use System\Twig\SecurityPolicy as TwigSecurityPolicy;
 use System\Models\EventLog;
 use System\Models\MailSetting;
 use System\Classes\CombineAssets;
 use Backend\Classes\WidgetManager;
 use October\Rain\Support\ModuleServiceProvider;
 use October\Rain\Router\Helper as RouterHelper;
 use Illuminate\Pagination\Paginator;
 use Illuminate\Support\Facades\Schema;
+use Twig\Extension\SandboxExtension;
 
 class ServiceProvider extends ModuleServiceProvider
 {
     /**
      * Register the service provider.
@@ -295,10 +297,11 @@ class ServiceProvider extends ModuleServiceProvider
          * Register system Twig environment
          */
         App::singleton('twig.environment', function ($app) {
             $twig = new TwigEnvironment(new TwigLoader, ['auto_reload' => true]);
             $twig->addExtension(new TwigExtension);
+            $twig->addExtension(new SandboxExtension(new TwigSecurityPolicy, true));
             return $twig;
         });
 
         /*
          * Register .htm extension for Twig views
diff --git a/modules/system/twig/SecurityPolicy.php b/modules/system/twig/SecurityPolicy.php
new file mode 100644
index 000000000..d215e328b
--- /dev/null
+++ b/modules/system/twig/SecurityPolicy.php
@@ -0,0 +1,61 @@
+<?php namespace System\Twig;
+
+use Twig\Markup;
+use Twig\Template;
+use Twig\Sandbox\SecurityPolicyInterface;
+use Twig\Sandbox\SecurityNotAllowedMethodError;
+use Twig\Sandbox\SecurityNotAllowedPropertyError;
+
+/**
+ * SecurityPolicy globally blocks accessibility of certain methods and properties.
+ *
+ * @package october\system
+ * @author Alexey Bobkov, Samuel Georges
+ */
+final class SecurityPolicy implements SecurityPolicyInterface
+{
+    protected $blockedProperties = [];
+
+    protected $blockedMethods = [
+        'addDynamicMethod',
+        'addDynamicProperty'
+    ];
+
+    public function __construct()
+    {
+        $this->setBlockedMethods($this->blockedMethods);
+    }
+
+    public function setBlockedMethods(array $methods)
+    {
+        foreach ($this->blockedMethods as $i => $m) {
+            $this->blockedMethods[$i] = strtr($m, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz');
+        }
+    }
+
+    public function checkSecurity($tags, $filters, $functions)
+    {
+    }
+
+    public function checkMethodAllowed($obj, $method)
+    {
+        if ($obj instanceof Template || $obj instanceof Markup) {
+            return;
+        }
+
+        $blockedMethod = strtr($method, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz');
+
+        if (in_array($blockedMethod, $this->blockedMethods)) {
+            $class = get_class($obj);
+            throw new SecurityNotAllowedMethodError(sprintf('Calling "%s" method on a "%s" object is blocked.', $method, $class), $class, $method);
+        }
+    }
+
+    public function checkPropertyAllowed($obj, $property)
+    {
+        if (in_array($property, $this->blockedProperties)) {
+            $class = get_class($obj);
+            throw new SecurityNotAllowedPropertyError(sprintf('Calling "%s" property on a "%s" object is blocked.', $property, $class), $class, $property);
+        }
+    }
+}

Fixed code: use Twig\Extension\SandboxExtension;
use System\Twig\SecurityPolicy;
        $this->twig->addExtension(new SandboxExtension(new SecurityPolicy, true));
use System\Twig\SecurityPolicy as TwigSecurityPolicy;
use Twig\Extension\SandboxExtension;
            $twig->addExtension(new SandboxExtension(new TwigSecurityPolicy, true));
<?php namespace System\Twig;

use Twig\Markup;
use Twig\Template;
use Twig\Sandbox\SecurityPolicyInterface;
use Twig\Sandbox\SecurityNotAllowedMethodError;
use Twig\Sandbox\SecurityNotAllowedPropertyError;

/**
 * SecurityPolicy globally blocks accessibility of certain methods and properties.
 *
 * @package october\system
 * @author Alexey Bobkov, Samuel Georges
 */
final class SecurityPolicy implements SecurityPolicyInterface
{
    protected $blockedProperties = [];

    protected $blockedMethods = [
        'addDynamicMethod',
        'addDynamicProperty'
    ];

    public function __construct()
    {
        $this->setBlockedMethods($this->blockedMethods);
    }

    public function setBlockedMethods(array $methods)
    {
        foreach ($this->blockedMethods as $i => $m) {
            $this->blockedMethods[$i] = strtr($m, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz');
        }
    }

    public function checkSecurity($tags, $filters, $functions)
    {
    }

    public function checkMethodAllowed($obj, $method)
    {
        if ($obj instanceof Template || $obj instanceof Markup) {
            return;
        }

        $blockedMethod = strtr($method, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz');

        if (in_array($blockedMethod, $this->blockedMethods)) {
            $class = get_class($obj);
            throw new SecurityNotAllowedMethodError(sprintf('Calling "%s" method on a "%s" object is blocked.', $method, $class), $class, $method);
        }
    }

    public function checkPropertyAllowed($obj, $property)
    {
        if (in_array($property, $this->blockedProperties)) {
            $class = get_class($obj);
            throw new SecurityNotAllowedPropertyError(sprintf('Calling "%s" property on a "%s" object is blocked.', $property, $class), $class, $property);
        }
    }
}