diff --git a/src/Compile/ModifierCompiler.php b/src/Compile/ModifierCompiler.php index dfec3d777..33782ee84 100644 --- a/src/Compile/ModifierCompiler.php +++ b/src/Compile/ModifierCompiler.php @@ -48,9 +48,10 @@ public function compile($args, \Smarty\Compiler\Template $compiler, $parameter = $modifier_params[0] = $output; $params = implode(',', $modifier_params); + $securityPolicy = $compiler->getSmarty()->getSecurityPolicy(); - if (!is_object($compiler->getSmarty()->security_policy) - || $compiler->getSmarty()->security_policy->isTrustedModifier($modifier, $compiler) + if ($securityPolicy === null + || $securityPolicy->isTrustedModifier($modifier, $compiler) ) { if ($handler = $compiler->getModifierCompiler($modifier)) { diff --git a/src/Compile/SpecialVariableCompiler.php b/src/Compile/SpecialVariableCompiler.php index 2b6cf4330..98f88ce00 100644 --- a/src/Compile/SpecialVariableCompiler.php +++ b/src/Compile/SpecialVariableCompiler.php @@ -45,8 +45,9 @@ public function compile($args, \Smarty\Compiler\Template $compiler, $parameter = if ($variable === false) { $compiler->trigger_template_error("special \$Smarty variable name index can not be variable", null, true); } - if (!isset($compiler->getSmarty()->security_policy) - || $compiler->getSmarty()->security_policy->isTrustedSpecialSmartyVar($variable, $compiler) + $securityPolicy = $compiler->getSmarty()->getSecurityPolicy(); + if ($securityPolicy === null + || $securityPolicy->isTrustedSpecialSmartyVar($variable, $compiler) ) { switch ($variable) { case 'foreach': @@ -58,8 +59,8 @@ public function compile($args, \Smarty\Compiler\Template $compiler, $parameter = case 'now': return 'time()'; case 'cookies': - if (isset($compiler->getSmarty()->security_policy) - && !$compiler->getSmarty()->security_policy->allow_super_globals + if ($securityPolicy !== null + && !$securityPolicy->allow_super_globals ) { $compiler->trigger_template_error("(secure mode) super globals not permitted"); break; @@ -72,8 +73,8 @@ public function compile($args, \Smarty\Compiler\Template $compiler, $parameter = case 'server': case 'session': case 'request': - if (isset($compiler->getSmarty()->security_policy) - && !$compiler->getSmarty()->security_policy->allow_super_globals + if ($securityPolicy !== null + && !$securityPolicy->allow_super_globals ) { $compiler->trigger_template_error("(secure mode) super globals not permitted"); break; @@ -83,7 +84,7 @@ public function compile($args, \Smarty\Compiler\Template $compiler, $parameter = case 'template': return '$_smarty_tpl->template_resource'; case 'template_object': - if (isset($compiler->getSmarty()->security_policy)) { + if ($securityPolicy !== null) { $compiler->trigger_template_error("(secure mode) template_object not permitted"); break; } @@ -93,8 +94,8 @@ public function compile($args, \Smarty\Compiler\Template $compiler, $parameter = case 'version': return "\\Smarty\\Smarty::SMARTY_VERSION"; case 'const': - if (isset($compiler->getSmarty()->security_policy) - && !$compiler->getSmarty()->security_policy->allow_constants + if ($securityPolicy !== null + && !$securityPolicy->allow_constants ) { $compiler->trigger_template_error("(secure mode) constants not permitted"); break; diff --git a/src/Compiler/Template.php b/src/Compiler/Template.php index efc52162b..6522449f7 100644 --- a/src/Compiler/Template.php +++ b/src/Compiler/Template.php @@ -606,8 +606,8 @@ public function processText($text) { */ public function getTagCompiler($tag): ?\Smarty\Compile\CompilerInterface { $tag = strtolower($tag); - - if (isset($this->smarty->security_policy) && !$this->smarty->security_policy->isTrustedTag($tag, $this)) { + $securityPolicy = $this->smarty->getSecurityPolicy(); + if ($securityPolicy !== null && !$securityPolicy->isTrustedTag($tag, $this)) { return null; } @@ -628,8 +628,8 @@ public function getTagCompiler($tag): ?\Smarty\Compile\CompilerInterface { * @return bool|\Smarty\Compile\Modifier\ModifierCompilerInterface tag compiler object or false if not found or untrusted by security policy */ public function getModifierCompiler($modifier) { - - if (isset($this->smarty->security_policy) && !$this->smarty->security_policy->isTrustedModifier($modifier, $this)) { + $securityPolicy = $this->smarty->getSecurityPolicy(); + if ($securityPolicy !== null && !$securityPolicy->isTrustedModifier($modifier, $this)) { return false; } @@ -1111,10 +1111,11 @@ private function compileTag2($tag, $args, $parameter) { // $args contains the attributes parsed and compiled by the lexer/parser $this->handleNocacheFlag($args); + $securityPolicy = $this->smarty->getSecurityPolicy(); // compile built-in tags if ($tagCompiler = $this->getTagCompiler($tag)) { - if (!isset($this->smarty->security_policy) || $this->smarty->security_policy->isTrustedTag($tag, $this)) { + if ($securityPolicy === null || $securityPolicy->isTrustedTag($tag, $this)) { $this->tag_nocache = $this->tag_nocache | !$tagCompiler->isCacheable(); $_output = $tagCompiler->compile($args, $this, $parameter); if (!empty($parameter['modifierlist'])) { @@ -1147,7 +1148,7 @@ private function compileTag2($tag, $args, $parameter) { // check if tag is a function if ($this->smarty->getFunctionHandler($tag)) { - if (!isset($this->smarty->security_policy) || $this->smarty->security_policy->isTrustedTag($tag, $this)) { + if ($securityPolicy === null || $securityPolicy->isTrustedTag($tag, $this)) { return (new \Smarty\Compile\PrintExpressionCompiler())->compile( ['nofilter'], // functions are never auto-escaped $this, @@ -1158,7 +1159,7 @@ private function compileTag2($tag, $args, $parameter) { // check if tag is a block if ($this->smarty->getBlockHandler($base_tag)) { - if (!isset($this->smarty->security_policy) || $this->smarty->security_policy->isTrustedTag($base_tag, $this)) { + if ($securityPolicy === null || $securityPolicy->isTrustedTag($base_tag, $this)) { return $this->blockCompiler->compile($args, $this, $parameter, $tag, $base_tag); } } diff --git a/src/Debug.php b/src/Debug.php index ab1a88779..7f287d5e7 100644 --- a/src/Debug.php +++ b/src/Debug.php @@ -195,7 +195,7 @@ public function display_debug($obj, bool $full = false) // copy the working dirs from application $debObj->setCompileDir($smarty->getCompileDir()); $debObj->compile_check = Smarty::COMPILECHECK_ON; - $debObj->security_policy = null; + $debObj->setSecurityPolicy(null); $debObj->debugging = false; $debObj->debugging_ctrl = 'NONE'; $debObj->error_reporting = E_ALL & ~E_NOTICE; diff --git a/src/FunctionHandler/Fetch.php b/src/FunctionHandler/Fetch.php index d10ef668f..b3cd67df4 100644 --- a/src/FunctionHandler/Fetch.php +++ b/src/FunctionHandler/Fetch.php @@ -33,15 +33,16 @@ public function handle($params, Template $template) { if ($protocol !== false) { $protocol = strtolower(substr($params['file'], 0, $protocol)); } - if (isset($template->getSmarty()->security_policy)) { + $securityPolicy = $template->getSmarty()->getSecurityPolicy(); + if ($securityPolicy !== null) { if ($protocol) { // remote resource (or php stream, …) - if (!$template->getSmarty()->security_policy->isTrustedUri($params['file'])) { + if (!$securityPolicy->isTrustedUri($params['file'])) { return; } } else { // local file - if (!$template->getSmarty()->security_policy->isTrustedResourceDir($params['file'])) { + if (!$securityPolicy->isTrustedResourceDir($params['file'])) { return; } } diff --git a/src/FunctionHandler/HtmlImage.php b/src/FunctionHandler/HtmlImage.php index 9cb087456..6184bc70f 100644 --- a/src/FunctionHandler/HtmlImage.php +++ b/src/FunctionHandler/HtmlImage.php @@ -97,15 +97,16 @@ public function handle($params, Template $template) { if ($protocol !== false) { $protocol = strtolower(substr($params['file'], 0, $protocol)); } - if (isset($template->getSmarty()->security_policy)) { + $securityPolicy = $template->getSmarty()->getSecurityPolicy(); + if ($securityPolicy !== null) { if ($protocol) { // remote resource (or php stream, …) - if (!$template->getSmarty()->security_policy->isTrustedUri($params['file'])) { + if (!$securityPolicy->isTrustedUri($params['file'])) { return; } } else { // local file - if (!$template->getSmarty()->security_policy->isTrustedResourceDir($_image_path)) { + if (!$securityPolicy->isTrustedResourceDir($_image_path)) { return; } } diff --git a/src/Parser/TemplateParser.php b/src/Parser/TemplateParser.php index 772df98d0..e879436fa 100644 --- a/src/Parser/TemplateParser.php +++ b/src/Parser/TemplateParser.php @@ -157,7 +157,7 @@ public function __construct(Lexer $lex, TemplateCompiler $compiler) $this->compiler = $compiler; $this->template = $this->compiler->getTemplate(); $this->smarty = $this->template->getSmarty(); - $this->security = $this->smarty->security_policy ?? false; + $this->security = $this->smarty->getSecurityPolicy() ?? false; $this->current_buffer = $this->root_buffer = new TemplateParseTree(); } diff --git a/src/Parser/TemplateParser.y b/src/Parser/TemplateParser.y index 544148f13..4d9599511 100644 --- a/src/Parser/TemplateParser.y +++ b/src/Parser/TemplateParser.y @@ -164,7 +164,7 @@ class TemplateParser $this->compiler = $compiler; $this->template = $this->compiler->getTemplate(); $this->smarty = $this->template->getSmarty(); - $this->security = $this->smarty->security_policy ?? false; + $this->security = $this->smarty->getSecurityPolicy() ?? false; $this->current_buffer = $this->root_buffer = new TemplateParseTree(); } diff --git a/src/Resource/BasePlugin.php b/src/Resource/BasePlugin.php index 6d2222237..08a1600ed 100644 --- a/src/Resource/BasePlugin.php +++ b/src/Resource/BasePlugin.php @@ -86,8 +86,9 @@ public static function load(Smarty $smarty, $type) $_known_stream = stream_get_wrappers(); if (in_array($type, $_known_stream)) { // is known stream - if (is_object($smarty->security_policy)) { - $smarty->security_policy->isTrustedStream($type); + $securityPolicy = $smarty->getSecurityPolicy(); + if ($securityPolicy !== null) { + $securityPolicy->isTrustedStream($type); } return $smarty->_resource_handlers[ $type ] = new StreamPlugin(); } diff --git a/src/Resource/FilePlugin.php b/src/Resource/FilePlugin.php index 0033c8348..8dc0b4426 100644 --- a/src/Resource/FilePlugin.php +++ b/src/Resource/FilePlugin.php @@ -40,8 +40,9 @@ public function populate(Source $source, ?Template $_template = null) { ); if ($path = $this->getFilePath($source->name, $source->getSmarty(), $source->isConfig)) { - if (isset($source->getSmarty()->security_policy) && is_object($source->getSmarty()->security_policy)) { - $source->getSmarty()->security_policy->isTrustedResourceDir($path, $source->isConfig); + $securityPolicy = $source->getSmarty()->getSecurityPolicy(); + if ($securityPolicy !== null) { + $securityPolicy->isTrustedResourceDir($path, $source->isConfig); } $source->exists = true; $source->timestamp = filemtime($path); diff --git a/src/Security.php b/src/Security.php index 250b3bca7..db1c70603 100644 --- a/src/Security.php +++ b/src/Security.php @@ -508,7 +508,7 @@ private function _checkDir($filepath, $dirs) { */ public static function enableSecurity(Smarty $smarty, $security_class) { if ($security_class instanceof Security) { - $smarty->security_policy = $security_class; + $smarty->setSecurityPolicy($security_class); return $smarty; } elseif (is_object($security_class)) { throw new Exception("Class '" . get_class($security_class) . "' must extend \\Smarty\\Security."); @@ -521,7 +521,7 @@ public static function enableSecurity(Smarty $smarty, $security_class) { } elseif ($security_class !== Security::class && !is_subclass_of($security_class, Security::class)) { throw new Exception("Class '$security_class' must extend " . Security::class . "."); } else { - $smarty->security_policy = new $security_class($smarty); + $smarty->setSecurityPolicy(new $security_class($smarty)); } return $smarty; } diff --git a/src/Smarty.php b/src/Smarty.php index 73df97eda..67f59484a 100644 --- a/src/Smarty.php +++ b/src/Smarty.php @@ -246,9 +246,9 @@ class Smarty extends \Smarty\TemplateBase { /** * implementation of security class * - * @var \Smarty\Security + * @var \Smarty\Security|null */ - public $security_policy = null; + private $security_policy = null; /** * debug mode @@ -594,10 +594,31 @@ public function enableSecurity($security_class = null) { * @return static current Smarty instance for chaining */ public function disableSecurity() { - $this->security_policy = null; + $this->setSecurityPolicy(null); return $this; } + /** + * Set security policy + * + * @param \Smarty\Security|null $policy Security policy instance or null to disable + * + * @return static current Smarty instance for chaining + */ + public function setSecurityPolicy(?\Smarty\Security $policy) { + $this->security_policy = $policy; + return $this; + } + + /** + * Get security policy + * + * @return \Smarty\Security|null Current security policy instance or null if disabled + */ + public function getSecurityPolicy(): ?\Smarty\Security { + return $this->security_policy; + } + /** * Add template directory(s) * @@ -2231,5 +2252,28 @@ public function setCacheModifiedCheck($cache_modified_check): void { $this->cache_modified_check = (bool) $cache_modified_check; } + /** + * Backward compatibility for security_policy property assignment with type validation + * + * @param string $name Property name + * @param mixed $value Property value + * + * @return void + * @throws \Smarty\Exception if security_policy value is invalid + */ + public function __set($name, $value) { + if ($name === 'security_policy') { + if ($value !== null && !($value instanceof \Smarty\Security)) { + $given = is_object($value) ? get_class($value) : gettype($value); + throw new \Smarty\Exception( + 'security_policy must be null or \Smarty\Security, ' . $given . ' given' + ); + } + $this->setSecurityPolicy($value); + return; + } + $this->$name = $value; + } + } diff --git a/src/Template.php b/src/Template.php index 242fb2388..536580186 100644 --- a/src/Template.php +++ b/src/Template.php @@ -135,8 +135,9 @@ public function __construct( $this->source = $_isConfig ? Config::load($this) : Source::load($this); $this->compiled = Compiled::load($this); - if ($smarty->security_policy) { - $smarty->security_policy->registerCallBacks($this); + $securityPolicy = $smarty->getSecurityPolicy(); + if ($securityPolicy !== null) { + $securityPolicy->registerCallBacks($this); } } diff --git a/tests/UnitTests/SecurityTests/SecurityTest.php b/tests/UnitTests/SecurityTests/SecurityTest.php index 9996f2252..81b15cbf9 100644 --- a/tests/UnitTests/SecurityTests/SecurityTest.php +++ b/tests/UnitTests/SecurityTests/SecurityTest.php @@ -30,7 +30,7 @@ public function testInit() */ public function testSecurityLoaded() { - $this->assertTrue(is_object($this->smarty->security_policy)); + $this->assertTrue($this->smarty->getSecurityPolicy() instanceof \Smarty\Security); } /** @@ -58,7 +58,7 @@ public function testTrustedModifier() */ public function testNotTrustedModifier() { - $this->smarty->security_policy->disabled_modifiers[] = 'escape'; + $this->smarty->getSecurityPolicy()->disabled_modifiers[] = 'escape'; $this->expectException(\Smarty\Exception::class); $this->expectExceptionMessage('modifier \'escape\' disabled by security setting'); @$this->smarty->fetch('string:{assign var=foo value=[1,2,3,4,5]}{$foo|escape}'); @@ -69,7 +69,7 @@ public function testNotTrustedModifier() */ public function testAllowedTags1() { - $this->smarty->security_policy->allowed_tags = array('counter'); + $this->smarty->getSecurityPolicy()->allowed_tags = array('counter'); $this->assertEquals("1", $this->smarty->fetch('string:{counter start=1}')); } @@ -82,7 +82,7 @@ public function testNotAllowedTags2() { $this->expectException(\Smarty\Exception::class); $this->expectExceptionMessage('tag \'cycle\' not allowed by security setting'); - $this->smarty->security_policy->allowed_tags = array('counter'); + $this->smarty->getSecurityPolicy()->allowed_tags = array('counter'); $this->smarty->fetch('string:{counter}{cycle values="1,2"}'); } @@ -95,7 +95,7 @@ public function testDisabledTags() { $this->expectException(\Smarty\Exception::class); $this->expectExceptionMessage('tag \'cycle\' disabled by security setting'); - $this->smarty->security_policy->disabled_tags = array('cycle'); + $this->smarty->getSecurityPolicy()->disabled_tags = array('cycle'); $this->smarty->fetch('string:{counter}{cycle values="1,2"}'); } @@ -105,14 +105,14 @@ public function testDisabledTags() public function testAllowedModifier1() { error_reporting(E_ALL & E_STRICT); - $this->smarty->security_policy->allowed_modifiers = array('capitalize'); + $this->smarty->getSecurityPolicy()->allowed_modifiers = array('capitalize'); $this->assertEquals("Hello World", $this->smarty->fetch('string:{"hello world"|capitalize}')); error_reporting(E_ALL | E_STRICT); } public function testAllowedModifier2() { - $this->smarty->security_policy->allowed_modifiers = array('upper'); + $this->smarty->getSecurityPolicy()->allowed_modifiers = array('upper'); $this->assertEquals("HELLO WORLD", $this->smarty->fetch('string:{"hello world"|upper}')); } @@ -125,7 +125,7 @@ public function testNotAllowedModifier() { $this->expectException(\Smarty\Exception::class); $this->expectExceptionMessage('modifier \'lower\' not allowed by security setting'); - $this->smarty->security_policy->allowed_modifiers = array('upper'); + $this->smarty->getSecurityPolicy()->allowed_modifiers = array('upper'); $this->smarty->fetch('string:{"hello"|upper}{"world"|lower}'); } @@ -138,7 +138,7 @@ public function testDisabledModifier() { $this->expectException(\Smarty\Exception::class); $this->expectExceptionMessage('modifier \'lower\' disabled by security setting'); - $this->smarty->security_policy->disabled_modifiers = array('lower'); + $this->smarty->getSecurityPolicy()->disabled_modifiers = array('lower'); $this->smarty->fetch('string:{"hello"|upper}{"world"|lower}'); } @@ -175,7 +175,7 @@ public function testStandardDirectory() */ public function testTrustedDirectory() { - $this->smarty->security_policy->secure_dir = array('.' . DIRECTORY_SEPARATOR . 'templates_2' . DIRECTORY_SEPARATOR); + $this->smarty->getSecurityPolicy()->secure_dir = array('.' . DIRECTORY_SEPARATOR . 'templates_2' . DIRECTORY_SEPARATOR); $this->assertEquals("hello world", $this->smarty->fetch('string:{include file="templates_2/hello.tpl"}')); } @@ -189,7 +189,7 @@ public function testNotTrustedDirectory() { $this->expectException(\Smarty\Exception::class); $this->expectExceptionMessage('not trusted file path'); - $this->smarty->security_policy->secure_dir = array(str_replace('\\', '/', __DIR__ . '/templates_3/')); + $this->smarty->getSecurityPolicy()->secure_dir = array(str_replace('\\', '/', __DIR__ . '/templates_3/')); $this->smarty->fetch('string:{include file="templates_2/hello.tpl"}'); } @@ -207,7 +207,7 @@ public function testDisabledTrustedDirectory() */ public function testTrustedStaticClass() { - $this->smarty->security_policy->static_classes = array('mysecuritystaticclass'); + $this->smarty->getSecurityPolicy()->static_classes = array('mysecuritystaticclass'); $tpl = $this->smarty->createTemplate('string:{mysecuritystaticclass::square(5)}'); $this->assertEquals('25', $this->smarty->fetch($tpl)); } @@ -221,7 +221,7 @@ public function testNotTrustedStaticClass() { $this->expectException(\Smarty\Exception::class); $this->expectExceptionMessage('access to static class \'mysecuritystaticclass\' not allowed by security setting'); - $this->smarty->security_policy->static_classes = array('null'); + $this->smarty->getSecurityPolicy()->static_classes = array('null'); $this->smarty->fetch('string:{mysecuritystaticclass::square(5)}'); } @@ -232,7 +232,7 @@ public function testNotTrustedStaticClassEval() { $this->expectException(\Smarty\Exception::class); $this->expectExceptionMessage('dynamic static class not allowed by security setting'); - $this->smarty->security_policy->static_classes = array('null'); + $this->smarty->getSecurityPolicy()->static_classes = array('null'); $this->smarty->fetch('string:{$test = "mysecuritystaticclass"}{$test::square(5)}'); } @@ -243,18 +243,18 @@ public function testNotTrustedStaticClassSmartyVar() { $this->expectException(\Smarty\Exception::class); $this->expectExceptionMessage('dynamic static class not allowed by security setting'); - $this->smarty->security_policy->static_classes = array('null'); + $this->smarty->getSecurityPolicy()->static_classes = array('null'); $this->smarty->fetch('string:{$smarty.template_object::square(5)}'); } public function testChangedTrustedDirectory() { - $this->smarty->security_policy->secure_dir = array( + $this->smarty->getSecurityPolicy()->secure_dir = array( '.' . DIRECTORY_SEPARATOR . 'templates_2' . DIRECTORY_SEPARATOR, ); $this->assertEquals("hello world", $this->smarty->fetch('string:{include file="templates_2/hello.tpl"}')); - $this->smarty->security_policy->secure_dir = array( + $this->smarty->getSecurityPolicy()->secure_dir = array( '.' . DIRECTORY_SEPARATOR . 'templates_2' . DIRECTORY_SEPARATOR, '.' . DIRECTORY_SEPARATOR . 'templates_3' . DIRECTORY_SEPARATOR, ); @@ -273,7 +273,7 @@ public function testTemplateTrustedStream() $fp = fopen("global://mytest", "r+"); fwrite($fp, 'hello world {$foo}'); fclose($fp); - $this->smarty->security_policy->streams= array('global'); + $this->smarty->getSecurityPolicy()->streams= array('global'); $tpl = $this->smarty->createTemplate('global:mytest'); $this->assertTrue($tpl->getSource()->exists); stream_wrapper_unregister("global"); @@ -292,7 +292,7 @@ public function testTemplateNotTrustedStream() $fp = fopen("global://mytest", "r+"); fwrite($fp, 'hello world {$foo}'); fclose($fp); - $this->smarty->security_policy->streams= array('notrusted'); + $this->smarty->getSecurityPolicy()->streams= array('notrusted'); $tpl = $this->smarty->createTemplate('global:mytest'); $this->assertTrue($tpl->getSource()->exists); stream_wrapper_unregister("global"); @@ -300,7 +300,7 @@ public function testTemplateNotTrustedStream() public function testTrustedUri() { - $this->smarty->security_policy->trusted_uri = array( + $this->smarty->getSecurityPolicy()->trusted_uri = array( '#https://s4otw4nhg.erteorteortert.nusuchtld$#i' ); @@ -318,7 +318,7 @@ public function testNotTrustedUri() { $this->expectException(\Smarty\Exception::class); $this->expectExceptionMessage('URI \'https://example.net\' not allowed by security setting'); - $this->smarty->security_policy->trusted_uri = []; + $this->smarty->getSecurityPolicy()->trusted_uri = []; $this->assertStringContainsString( 'Preface | Smarty', $this->smarty->fetch('string:{fetch file="https://example.net"}')