From 3532a05961daf93d90727dc25fe1ce556675b5e8 Mon Sep 17 00:00:00 2001 From: Ben Thomson Date: Wed, 16 Oct 2024 05:48:52 +0000 Subject: [PATCH] Backport fb88e6fab to 1.0 * Harden theme objects, prevent certain properties from being passed through to ThemeData object * Improve and properly scope Twig security policy - Block methods that write, delete or modify records and attributes in Database/Eloquent and Halcyon models - Block access to theme datasource - Prevent extensions from being created or directly interacted with (models and properties provided to extended objects should still be OK) Refs: https://github.com/wintercms/winter/commit/fb88e6fabde3b3278ce1844e581c87dcf7daee22#diff-347d3e6f6f84697f5be048027169529a5ed7e782fcf2dcf62dcdbf560a0a4f77 --- modules/cms/classes/Theme.php | 41 +++++---- modules/system/twig/SecurityPolicy.php | 120 +++++++++++++++++++++---- 2 files changed, 129 insertions(+), 32 deletions(-) diff --git a/modules/cms/classes/Theme.php b/modules/cms/classes/Theme.php index 7171205607..8972cf32eb 100644 --- a/modules/cms/classes/Theme.php +++ b/modules/cms/classes/Theme.php @@ -1,22 +1,24 @@ -$method(); + } + if ($this->hasCustomData()) { return $this->getCustomData()->{$name}; } @@ -596,6 +603,10 @@ public function __get($name) */ public function __isset($key) { + if (in_array(strtolower($key), ['id', 'path', 'dirname', 'config', 'formconfig', 'previewimageurl'])) { + return true; + } + if ($this->hasCustomData()) { $theme = $this->getCustomData(); return $theme->offsetExists($key); diff --git a/modules/system/twig/SecurityPolicy.php b/modules/system/twig/SecurityPolicy.php index 4e7adb38ce..f09a70c7b0 100644 --- a/modules/system/twig/SecurityPolicy.php +++ b/modules/system/twig/SecurityPolicy.php @@ -1,35 +1,94 @@ List of forbidden methods, grouped by applicable instance. */ protected $blockedMethods = [ - // \Winter\Storm\Extension\ExtendableTrait - 'addDynamicMethod', - 'addDynamicProperty', - - // \Winter\Storm\Support\Traits\Emitter - 'bindEvent', - 'bindEventOnce', + '*' => [ + // Prevent accessing Twig itself + 'getTwig', + // Prevent extensions of any objects + 'addDynamicMethod', + 'addDynamicProperty', + 'extendClassWith', + 'getClassExtension', + 'extendableSet', + // Prevent binding to events + 'bindEvent', + 'bindEventOnce', + ], + // Prevent some controller methods + Controller::class => [ + 'runPage', + 'renderPage', + 'getLoader', + ], + // Prevent model data modification + DbModel::class => [ + 'fill', + 'setAttribute', + 'setRawAttributes', + 'save', + 'push', + 'update', + 'delete', + 'forceDelete', + ], + HalcyonModel::class => [ + 'fill', + 'setAttribute', + 'setRawAttributes', + 'setSettingsAttribute', + 'setFileNameAttribute', + 'save', + 'push', + 'update', + 'delete', + 'forceDelete', + ], + DatasourceInterface::class => [ + 'insert', + 'update', + 'delete', + 'forceDelete', + 'write', + 'usingSource', + 'pushToSource', + 'removeFromSource', + ], + Theme::class => [ + 'setDirName', + 'registerHalcyonDatasource', + 'getDatasource' + ], + ]; - // Eloquent & Halcyon data modification - 'insert', - 'update', - 'delete', + /** + * @var array List of forbidden properties, grouped by applicable instance. + */ + protected $blockedProperties = [ + Theme::class => [ + 'datasource', + ], ]; /** @@ -37,8 +96,12 @@ final class SecurityPolicy implements SecurityPolicyInterface */ public function __construct() { - foreach ($this->blockedMethods as $i => $m) { - $this->blockedMethods[$i] = strtolower($m); + foreach ($this->blockedMethods as $type => $methods) { + $this->blockedMethods[$type] = array_map('strtolower', $methods); + } + + foreach ($this->blockedProperties as $type => $properties) { + $this->blockedProperties[$type] = array_map('strtolower', $properties); } } @@ -65,6 +128,19 @@ public function checkSecurity($tags, $filters, $functions) */ public function checkPropertyAllowed($obj, $property) { + // No need to check Twig internal objects + if ($obj instanceof Template || $obj instanceof Markup) { + return; + } + + $property = strtolower($property); + + foreach ($this->blockedProperties as $type => $properties) { + if ($obj instanceof $type && in_array($property, $properties)) { + $class = get_class($obj); + throw new SecurityNotAllowedPropertyError(sprintf('Getting "%s" property in a "%s" object is blocked.', $property, $class), $class, $property); + } + } } /** @@ -81,10 +157,20 @@ public function checkMethodAllowed($obj, $method) return; } - $blockedMethod = strtolower($method); - if (in_array($blockedMethod, $this->blockedMethods)) { + $method = strtolower($method); + + if ( + in_array($method, $this->blockedMethods['*']) + ) { $class = get_class($obj); throw new SecurityNotAllowedMethodError(sprintf('Calling "%s" method on a "%s" object is blocked.', $method, $class), $class, $method); } + + foreach ($this->blockedMethods as $type => $methods) { + if ($obj instanceof $type && in_array($method, $methods)) { + $class = get_class($obj); + throw new SecurityNotAllowedMethodError(sprintf('Calling "%s" method on a "%s" object is blocked.', $method, $class), $class, $method); + } + } } }