Compare commits

..

2 Commits

Author SHA1 Message Date
Simon Wisselink 1bca68beec Fix TypeError for non-array static_classes in Security policy 2026-06-29 12:34:58 +02:00
Simon Wisselink 3c9f77a2e0 Security: validate nested stream wrapper in stream: resource (CWE-22) (#1195)
The built-in stream: resource type let a template bypass Security stream
restrictions. BasePlugin::load() matches the 'stream' sysplugin before the
stream_get_wrappers()/isTrustedStream() check, so a resource such as
stream:php://filter/read=convert.base64-encode/resource=/path was opened by
StreamPlugin::getContent() via fopen() on the nested php:// wrapper without
ever validating it. This bypassed Security::$streams (including
Security::$streams = null) and allowed reading arbitrary local files.

Parse the wrapper scheme from the resolved path in StreamPlugin::getContent()
and validate it with Security::isTrustedStream() before fopen(), giving the
stream: resource the same check the direct wrapper path already receives.

Adds regression tests covering the disabled-streams bypass, the
not-on-allowlist case, and a positive test that an explicitly allowed wrapper
still works.
2026-06-29 11:47:32 +02:00
3 changed files with 40 additions and 2 deletions
@@ -0,0 +1 @@
- Fixed a `TypeError` on PHP 8 when `Security::$static_classes` was set to a non-array value (e.g. the string `'none'`) to disable static class access; any non-array value now cleanly denies access. Use `Security::$static_classes = null` to disable access to all static classes.
+6 -2
View File
@@ -52,7 +52,7 @@ class Security {
/**
* This is an array of trusted static classes.
* If empty access to all static classes is allowed.
* If set to 'none' none is allowed.
* To disable access to all static classes set $static_classes = null.
*
* @var array
*/
@@ -206,7 +206,11 @@ class Security {
* @return boolean true if class is trusted
*/
public function isTrustedStaticClass($class_name, $compiler) {
if (isset($this->static_classes)
// Only an array enables access: an empty array allows all classes, a
// populated array is an allowlist. Any other value (null, or the
// documented "none") denies all. Using is_array() rather than isset()
// also avoids a PHP 8 TypeError from passing a non-array to in_array().
if (is_array($this->static_classes)
&& (empty($this->static_classes) || in_array($class_name, $this->static_classes))
) {
return true;
@@ -243,6 +243,39 @@ class SecurityTest extends PHPUnit_Smarty
$this->smarty->fetch('string:{$smarty.template_object::square(5)}');
}
/**
* The default (empty array) allows access to all static classes. Documents
* the backwards-compatible behaviour.
*/
public function testStaticClassAllowedByDefault()
{
$this->smarty->security_policy->static_classes = array();
$this->assertEquals('25', $this->smarty->fetch('string:{mysecuritystaticclass::square(5)}'));
}
/**
* Setting static_classes to null disables access to all static classes.
*/
public function testStaticClassDeniedWhenNull()
{
$this->expectException(\Smarty\Exception::class);
$this->expectExceptionMessage("access to static class 'mysecuritystaticclass' not allowed by security setting");
$this->smarty->security_policy->static_classes = null;
$this->smarty->fetch('string:{mysecuritystaticclass::square(5)}');
}
/**
* Regression: a non-array value such as the string 'none' must deny access
* cleanly instead of raising a PHP 8 TypeError from in_array().
*/
public function testStaticClassDeniedWhenNonArray()
{
$this->expectException(\Smarty\Exception::class);
$this->expectExceptionMessage("access to static class 'mysecuritystaticclass' not allowed by security setting");
$this->smarty->security_policy->static_classes = 'none';
$this->smarty->fetch('string:{mysecuritystaticclass::square(5)}');
}
public function testChangedTrustedDirectory()
{
$this->smarty->security_policy->secure_dir = array(