Compare commits

..

19 Commits

Author SHA1 Message Date
Simon Wisselink 838d6ad1c6 Merge branch 'release/4.5.7' into support/4 2026-06-29 10:48:56 +02:00
Simon Wisselink 187f7b6246 version bump 2026-06-29 10:48:54 +02:00
Simon Wisselink a1ccdb0518 Port security fixes from v5.8.2 to Smarty v4. (#1194) 2026-06-29 10:48:33 +02:00
Simon Wisselink c285f84fc6 Reproduce block override leakage in template inheritance 2026-06-23 22:53:43 +02:00
Simon Wisselink a8d77c8666 Merge branch 'release/4.5.6' into support/4 2025-08-26 10:37:46 +02:00
Simon Wisselink 7721e5d786 version bump 2025-08-26 10:37:44 +02:00
Simon Wisselink 3e6a478e93 Fixed escaping of array/object keys in debug_print_var 2025-08-26 10:37:17 +02:00
Shad c438c79d7d add count and debug_print_var modifier doc (#1116) 2025-03-17 09:18:09 +01:00
Shad e1ef353ccf add missing closing tr tag (#1117)
Add missing end tag for 'assigned template variables' section.
backport of d6153d4
2025-03-17 09:15:32 +01:00
Simon Wisselink 5cb412d040 Bugfix/1100 modifier called like a function compiled to modifier name not callback (#1101)
* Fixed that modifiers called like function would be compiled to modifier name instead of calling the registered callback
2025-02-13 23:21:21 +01:00
Simon Wisselink 272a407e9d change SMARTY_VERSION to $smarty.version.
Fixes #1073
2024-12-28 17:39:36 +01:00
Simon Wisselink c4851c12e3 Merge branch 'release/4.5.5' into support/4 2024-11-21 23:06:22 +01:00
Simon Wisselink 48f9d291de version bump 2024-11-21 23:06:19 +01:00
Simon Wisselink 4fbff32417 changelog 2024-11-21 23:06:02 +01:00
Jonathan Lelievre bf87aee3f0 Add PHP 8.4 support to Smarty (#1084)
* Add nullable types

* Removing deprecated E_STRICT constant from test suite.

* Added PHP 8.4 RC1 as test image

* PHP version bump in the README.md.

* Added PHP 8.4 to CI

* Add running tests for PHP 8.4
2024-11-21 23:04:22 +01:00
Simon Wisselink ffc4415a4f Add unit tests for short hand template function definition and shorthand template function calls. 2024-10-06 22:03:23 +02:00
Simon Wisselink c11676e85a Merge branch 'release/4.5.4' into support/4 2024-08-14 22:04:35 +02:00
Simon Wisselink 44a08bea7c version bump 2024-08-14 22:04:33 +02:00
Simon Wisselink 2adb8801a5 Fixed that using count() would trigger a deprecation notice. (#1054) 2024-08-14 10:58:02 +02:00
63 changed files with 718 additions and 152 deletions
+4
View File
@@ -33,6 +33,7 @@ jobs:
- "8.1"
- "8.2"
- "8.3"
- "8.4"
compiler:
- default
@@ -50,6 +51,9 @@ jobs:
- os: ubuntu-latest
php-version: "8.3"
compiler: jit
- os: ubuntu-latest
php-version: "8.4"
compiler: jit
steps:
- name: Checkout
uses: actions/checkout@v3
+21
View File
@@ -6,6 +6,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [4.5.7] - 2026-06-29
- Security: prevent symlinks inside a trusted `secure_dir`/template directory from being used to read files outside of it (CWE-22 path traversal), affecting `{include}` and `{fetch}` of local files
- Security: `{html_image}` now escapes the `file`, `path_prefix`, `href`/`link`, `width` and `height` attributes (it already escaped `alt` and pass-through attributes), and `{html_select_date}` casts `day_size`/`month_size`/`year_size` to int (matching `{html_select_time}`), preventing untrusted values passed into these attributes from breaking out of the generated HTML (CWE-79)
- Security: `{fetch}` no longer follows HTTP redirects for remote resources while a security policy is active, preventing an open redirect on a trusted host from bypassing `trusted_uri` (CWE-918 server-side request forgery)
## [4.5.6] - 2025-08-26
- Fixed that modifiers called like function would be compiled to modifier name instead of calling the registered callback [#1100](https://github.com/smarty-php/smarty/issues/1100)
- Replace SMARTY_VERSION constant with $smarty.version in debug.tpl [#1073](https://github.com/smarty-php/smarty/issues/1073)
- Fixed escaping of array/object keys in debug_print_var
## [4.5.5] - 2024-11-21
- Support the deprecations introduced in PHP 8.4 and added tests for PHP 8.4 [#1084](https://github.com/smarty-php/smarty/pull/1084)
## [4.5.4] - 2024-08-14
- Fixed that using `count()` would trigger a deprecation notice. [#813](https://github.com/smarty-php/smarty/issues/813)
## [4.5.3] - 2024-05-28
- Fixed a code injection vulnerability in extends-tag. This addresses CVE-2024-35226.
+1 -1
View File
@@ -7,7 +7,7 @@ Smarty is a template engine for PHP, facilitating the separation of presentation
Read the [documentation](https://smarty-php.github.io/smarty/) to find out how to use it.
## Requirements
Smarty can be run with PHP 7.1 to PHP 8.3.
Smarty can be run with PHP 7.1 to PHP 8.4.
## Installation
Smarty versions 3.1.11 or later can be installed with [Composer](https://getcomposer.org/).
-1
View File
@@ -1 +0,0 @@
- Fixed that using `count()` would trigger a deprecation notice. [#813](https://github.com/smarty-php/smarty/issues/813)
+3 -3
View File
@@ -13,12 +13,12 @@ class Smarty_Resource_Extendsall extends Smarty_Internal_Resource_Extends
/**
* populate Source Object with meta data from Resource
*
* @param Smarty_Template_Source $source source object
* @param Smarty_Internal_Template $_template template object
* @param Smarty_Template_Source $source source object
* @param Smarty_Internal_Template|null $_template template object
*
* @return void
*/
public function populate(Smarty_Template_Source $source, Smarty_Internal_Template $_template = null)
public function populate(Smarty_Template_Source $source, ?Smarty_Internal_Template $_template = null)
{
$uid = '';
$sources = array();
+6 -1
View File
@@ -46,4 +46,9 @@ services:
extends:
service: base
build:
dockerfile: ./utilities/testrunners/php83/Dockerfile
dockerfile: ./utilities/testrunners/php83/Dockerfile
php84:
extends:
service: base
build:
dockerfile: ./utilities/testrunners/php84/Dockerfile
@@ -12,10 +12,12 @@ These parameters follow the modifier name and are separated by a `:`
- [capitalize](language-modifier-capitalize.md)
- [cat](language-modifier-cat.md)
- [count](language-modifier-count.md)
- [count_characters](language-modifier-count-characters.md)
- [count_paragraphs](language-modifier-count-paragraphs.md)
- [count_sentences](language-modifier-count-sentences.md)
- [count_words](language-modifier-count-words.md)
- [debug_print_var](language-modifier-debug-print-var.md)
- [date_format](language-modifier-date-format.md)
- [default](language-modifier-default.md)
- [escape](language-modifier-escape.md)
@@ -0,0 +1,21 @@
# count
Returns the number of elements in an array (or Countable object). Will return 0 for null.
Returns 1 for any other type (such as a string).
If the optional mode parameter is set to 1, count() will recursively count the array.
This is particularly useful for counting all the elements of a multidimensional array.
## Basic usage
```smarty
{if $myVar|count > 3}4 or more{/if}
{if count($myVar) > 3}4 or more{/if}
```
## Parameters
| Parameter | Type | Required | Description |
|-----------|------|----------|--------------------------------------------------------|
| 1 | int | No | If set to 1, count() will recursively count the array. |
@@ -0,0 +1,26 @@
# debug_print_var
Returns the value of the given variable in a human-readable format in HTML.
Used in the [debug console](../chapter-debugging-console.md), but you can also use it in your template
while developing to see what is going on under the hood.
> **Note**
>
> Use for debugging only! Since you may accidentally reveal sensitive information or introduce vulnerabilities such as XSS using this
method never use it in production.
## Basic usage
```smarty
{$myVar|debug_print_var}
```
## Parameters
| Parameter | Type | Required | Description |
|-----------|------|----------|------------------------------------------------------------------------|
| 1 | int | No | maximum recursion depth if $var is an array or object (defaults to 10) |
| 2 | int | No | maximum string length if $var is a string (defaults to 40) |
+1 -1
View File
@@ -1,7 +1,7 @@
# Getting started
## Requirements
Smarty can be run with PHP 7.1 to PHP 8.3.
Smarty can be run with PHP 7.1 to PHP 8.4.
## Installation
Smarty can be installed with [Composer](https://getcomposer.org/).
+7 -7
View File
@@ -107,7 +107,7 @@ class Smarty extends Smarty_Internal_TemplateBase
/**
* smarty version
*/
const SMARTY_VERSION = '4.5.3';
const SMARTY_VERSION = '4.5.7';
/**
* define variable scopes
*/
@@ -1036,11 +1036,11 @@ class Smarty extends Smarty_Internal_TemplateBase
/**
* Get unique template id
*
* @param string $template_name
* @param null|mixed $cache_id
* @param null|mixed $compile_id
* @param null $caching
* @param \Smarty_Internal_Template $template
* @param string $template_name
* @param null|mixed $cache_id
* @param null|mixed $compile_id
* @param null $caching
* @param \Smarty_Internal_Template|null $template
*
* @return string
* @throws \SmartyException
@@ -1050,7 +1050,7 @@ class Smarty extends Smarty_Internal_TemplateBase
$cache_id = null,
$compile_id = null,
$caching = null,
Smarty_Internal_Template $template = null
?Smarty_Internal_Template $template = null
) {
$template_name = (strpos($template_name, ':') === false) ? "{$this->default_resource_type}:{$template_name}" :
$template_name;
+2 -1
View File
@@ -108,7 +108,7 @@
</head>
<body>
<h1>Smarty {Smarty::SMARTY_VERSION} Debug Console
<h1>Smarty {$smarty.version} Debug Console
- {if isset($template_name)}{$template_name|debug_print_var nofilter} {/if}{if !empty($template_data)}Total Time {$execution_time|string_format:"%.5f"}{/if}</h1>
{if !empty($template_data)}
@@ -144,6 +144,7 @@
{$vars['attributes']|debug_print_var nofilter}
{/if}
</td>
</tr>
{/foreach}
</table>
+18 -1
View File
@@ -191,7 +191,24 @@ function smarty_function_fetch($params, $template)
return;
}
} else {
$content = @file_get_contents($params[ 'file' ]);
if ($protocol && isset($template->smarty->security_policy)) {
// Remote resource (e.g. https://) reached through file_get_contents().
// isTrustedUri() only validates the initial URL, but file_get_contents()
// follows redirects by default, so an open redirect on an otherwise
// trusted host could be used to reach a non-trusted target (SSRF).
// Disable redirect-following while a security policy is in effect.
$context = stream_context_create(
array(
'http' => array(
'follow_location' => 0,
'max_redirects' => 1,
),
)
);
$content = @file_get_contents($params[ 'file' ], false, $context);
} else {
$content = @file_get_contents($params[ 'file' ]);
}
if ($content === false) {
throw new SmartyException("{fetch} cannot read resource '" . $params[ 'file' ] . "'");
}
+8 -3
View File
@@ -75,7 +75,7 @@ function smarty_function_html_image($params, Smarty_Internal_Template $template)
break;
case 'link':
case 'href':
$prefix = '<a href="' . $_val . '">';
$prefix = '<a href="' . smarty_function_escape_special_chars($_val) . '">';
$suffix = '</a>';
break;
default:
@@ -153,6 +153,11 @@ function smarty_function_html_image($params, Smarty_Internal_Template $template)
$width = round($width * $_resize);
$height = round($height * $_resize);
}
return $prefix . '<img src="' . $path_prefix . $file . '" alt="' . $alt . '" width="' . $width . '" height="' .
$height . '"' . $extra . ' />' . $suffix;
// $alt and the pass-through attributes ($extra) are already escaped above;
// escape the remaining value-context params at output time so untrusted
// values cannot break out of the attribute (CWE-79). The unescaped $file/
// $width/$height are still used for getimagesize()/DPI math above.
return $prefix . '<img src="' . smarty_function_escape_special_chars($path_prefix . $file) . '" alt="' . $alt
. '" width="' . smarty_function_escape_special_chars($width) . '" height="'
. smarty_function_escape_special_chars($height) . '"' . $extra . ' />' . $suffix;
}
+7 -3
View File
@@ -131,9 +131,6 @@ function smarty_function_html_select_date($params, Smarty_Internal_Template $tem
case 'day_value_format':
case 'month_format':
case 'month_value_format':
case 'day_size':
case 'month_size':
case 'year_size':
case 'all_extra':
case 'day_extra':
case 'month_extra':
@@ -151,6 +148,13 @@ function smarty_function_html_select_date($params, Smarty_Internal_Template $tem
case 'year_id':
$$_key = (string)$_value;
break;
case 'day_size':
case 'month_size':
case 'year_size':
// numeric HTML size attribute; cast to int (consistent with
// html_select_time) so it cannot break out of size="…" (CWE-79)
$$_key = (int)$_value;
break;
case 'display_days':
case 'display_months':
case 'display_years':
+2 -2
View File
@@ -31,7 +31,7 @@ function smarty_modifier_debug_print_var($var, $max = 10, $length = 40, $depth =
break;
}
foreach ($var as $curr_key => $curr_val) {
$results .= '<br>' . str_repeat('&nbsp;', $depth * 2) . '<b>' . strtr($curr_key, $_replace) .
$results .= '<br>' . str_repeat('&nbsp;', $depth * 2) . '<b>' . htmlspecialchars(strtr($curr_key, $_replace)) .
'</b> =&gt; ' .
smarty_modifier_debug_print_var($curr_val, $max, $length, ++$depth, $objects);
$depth--;
@@ -49,7 +49,7 @@ function smarty_modifier_debug_print_var($var, $max = 10, $length = 40, $depth =
}
$objects[] = $var;
foreach ($object_vars as $curr_key => $curr_val) {
$results .= '<br>' . str_repeat('&nbsp;', $depth * 2) . '<b> -&gt;' . strtr($curr_key, $_replace) .
$results .= '<br>' . str_repeat('&nbsp;', $depth * 2) . '<b> -&gt;' . htmlspecialchars(strtr($curr_key, $_replace)) .
'</b> = ' . smarty_modifier_debug_print_var($curr_val, $max, $length, ++$depth, $objects);
$depth--;
}
+4 -4
View File
@@ -44,15 +44,15 @@ abstract class Smarty_CacheResource
/**
* Read the cached template and process header
*
* @param Smarty_Internal_Template $_template template object
* @param Smarty_Template_Cached $cached cached object
* @param boolean $update flag if called because cache update
* @param Smarty_Internal_Template $_template template object
* @param Smarty_Template_Cached|null $cached cached object
* @param boolean $update flag if called because cache update
*
* @return boolean true or false if the cached content does not exist
*/
abstract public function process(
Smarty_Internal_Template $_template,
Smarty_Template_Cached $cached = null,
?Smarty_Template_Cached $cached = null,
$update = false
);
@@ -124,15 +124,15 @@ abstract class Smarty_CacheResource_Custom extends Smarty_CacheResource
/**
* Read the cached template and process the header
*
* @param \Smarty_Internal_Template $_smarty_tpl do not change variable name, is used by compiled template
* @param Smarty_Template_Cached $cached cached object
* @param boolean $update flag if called because cache update
* @param \Smarty_Internal_Template $_smarty_tpl do not change variable name, is used by compiled template
* @param Smarty_Template_Cached|null $cached cached object
* @param boolean $update flag if called because cache update
*
* @return boolean true or false if the cached content does not exist
*/
public function process(
Smarty_Internal_Template $_smarty_tpl,
Smarty_Template_Cached $cached = null,
?Smarty_Template_Cached $cached = null,
$update = false
) {
if (!$cached) {
@@ -88,15 +88,15 @@ abstract class Smarty_CacheResource_KeyValueStore extends Smarty_CacheResource
/**
* Read the cached template and process the header
*
* @param \Smarty_Internal_Template $_smarty_tpl do not change variable name, is used by compiled template
* @param Smarty_Template_Cached $cached cached object
* @param boolean $update flag if called because cache update
* @param \Smarty_Internal_Template $_smarty_tpl do not change variable name, is used by compiled template
* @param Smarty_Template_Cached|null $cached cached object
* @param boolean $update flag if called because cache update
*
* @return boolean true or false if the cached content does not exist
*/
public function process(
Smarty_Internal_Template $_smarty_tpl,
Smarty_Template_Cached $cached = null,
?Smarty_Template_Cached $cached = null,
$update = false
) {
if (!$cached) {
@@ -88,15 +88,15 @@ class Smarty_Internal_CacheResource_File extends Smarty_CacheResource
/**
* Read the cached template and process its header
*
* @param \Smarty_Internal_Template $_smarty_tpl do not change variable name, is used by compiled template
* @param Smarty_Template_Cached $cached cached object
* @param bool $update flag if called because cache update
* @param \Smarty_Internal_Template $_smarty_tpl do not change variable name, is used by compiled template
* @param Smarty_Template_Cached|null $cached cached object
* @param bool $update flag if called because cache update
*
* @return boolean true or false if the cached content does not exist
*/
public function process(
Smarty_Internal_Template $_smarty_tpl,
Smarty_Template_Cached $cached = null,
?Smarty_Template_Cached $cached = null,
$update = false
) {
$_smarty_tpl->cached->valid = false;
+2 -2
View File
@@ -190,7 +190,7 @@ abstract class Smarty_Internal_Data
*
* @return mixed variable value or or array of variables
*/
public function getTemplateVars($varName = null, Smarty_Internal_Data $_ptr = null, $searchParents = true)
public function getTemplateVars($varName = null, ?Smarty_Internal_Data $_ptr = null, $searchParents = true)
{
return $this->ext->getTemplateVars->getTemplateVars($this, $varName, $_ptr, $searchParents);
}
@@ -200,7 +200,7 @@ abstract class Smarty_Internal_Data
*
* @param \Smarty_Internal_Data|null $data
*/
public function _mergeVars(Smarty_Internal_Data $data = null)
public function _mergeVars(?Smarty_Internal_Data $data = null)
{
if (isset($data)) {
if (!empty($this->tpl_vars)) {
@@ -24,14 +24,14 @@ class Smarty_Internal_Method_CreateData
* @api Smarty::createData()
* @link https://www.smarty.net/docs/en/api.create.data.tpl
*
* @param \Smarty_Internal_TemplateBase|\Smarty_Internal_Template|\Smarty $obj
* @param \Smarty_Internal_Template|\Smarty_Internal_Data|\Smarty_Data|\Smarty $parent next higher level of Smarty
* variables
* @param string $name optional data block name
* @param \Smarty_Internal_TemplateBase|\Smarty_Internal_Template|\Smarty $obj
* @param \Smarty_Internal_Template|\Smarty_Internal_Data|\Smarty_Data|\Smarty|null $parent next higher level of Smarty
* variables
* @param string $name optional data block name
*
* @return \Smarty_Data data object
*/
public function createData(Smarty_Internal_TemplateBase $obj, Smarty_Internal_Data $parent = null, $name = null)
public function createData(Smarty_Internal_TemplateBase $obj, ?Smarty_Internal_Data $parent = null, $name = null)
{
/* @var Smarty $smarty */
$smarty = $obj->_getSmartyObj();
@@ -24,17 +24,17 @@ class Smarty_Internal_Method_GetTemplateVars
* @api Smarty::getTemplateVars()
* @link https://www.smarty.net/docs/en/api.get.template.vars.tpl
*
* @param \Smarty_Internal_Data|\Smarty_Internal_Template|\Smarty $data
* @param string $varName variable name or null
* @param \Smarty_Internal_Data|\Smarty_Internal_Template|\Smarty $_ptr optional pointer to data object
* @param bool $searchParents include parent templates?
* @param \Smarty_Internal_Data|\Smarty_Internal_Template|\Smarty $data
* @param string $varName variable name or null
* @param \Smarty_Internal_Data|\Smarty_Internal_Template|\Smarty|null $_ptr optional pointer to data object
* @param bool $searchParents include parent templates?
*
* @return mixed variable value or or array of variables
*/
public function getTemplateVars(
Smarty_Internal_Data $data,
$varName = null,
Smarty_Internal_Data $_ptr = null,
?Smarty_Internal_Data $_ptr = null,
$searchParents = true
) {
if (isset($varName)) {
@@ -87,7 +87,7 @@ class Smarty_Internal_Method_GetTemplateVars
public function _getVariable(
Smarty_Internal_Data $data,
$varName,
Smarty_Internal_Data $_ptr = null,
?Smarty_Internal_Data $_ptr = null,
$searchParents = true,
$errorEnable = true
) {
@@ -22,11 +22,11 @@ class Smarty_Internal_Resource_Eval extends Smarty_Resource_Recompiled
* populate Source Object with meta data from Resource
*
* @param Smarty_Template_Source $source source object
* @param Smarty_Internal_Template $_template template object
* @param Smarty_Internal_Template|null $_template template object
*
* @return void
*/
public function populate(Smarty_Template_Source $source, Smarty_Internal_Template $_template = null)
public function populate(Smarty_Template_Source $source, ?Smarty_Internal_Template $_template = null)
{
$source->uid = $source->filepath = sha1($source->name);
$source->timestamp = $source->exists = true;
@@ -28,11 +28,11 @@ class Smarty_Internal_Resource_Extends extends Smarty_Resource
* populate Source Object with meta data from Resource
*
* @param Smarty_Template_Source $source source object
* @param Smarty_Internal_Template $_template template object
* @param Smarty_Internal_Template|null $_template template object
*
* @throws SmartyException
*/
public function populate(Smarty_Template_Source $source, Smarty_Internal_Template $_template = null)
public function populate(Smarty_Template_Source $source, ?Smarty_Internal_Template $_template = null)
{
$uid = '';
$sources = array();
@@ -21,11 +21,11 @@ class Smarty_Internal_Resource_File extends Smarty_Resource
* populate Source Object with meta data from Resource
*
* @param Smarty_Template_Source $source source object
* @param Smarty_Internal_Template $_template template object
* @param Smarty_Internal_Template|null $_template template object
*
* @throws \SmartyException
*/
public function populate(Smarty_Template_Source $source, Smarty_Internal_Template $_template = null)
public function populate(Smarty_Template_Source $source, ?Smarty_Internal_Template $_template = null)
{
$source->filepath = $this->buildFilepath($source, $_template);
if ($source->filepath !== false) {
@@ -93,12 +93,12 @@ class Smarty_Internal_Resource_File extends Smarty_Resource
* build template filepath by traversing the template_dir array
*
* @param Smarty_Template_Source $source source object
* @param Smarty_Internal_Template $_template template object
* @param Smarty_Internal_Template|null $_template template object
*
* @return string fully qualified filepath
* @throws SmartyException
*/
protected function buildFilepath(Smarty_Template_Source $source, Smarty_Internal_Template $_template = null)
protected function buildFilepath(Smarty_Template_Source $source, ?Smarty_Internal_Template $_template = null)
{
$file = $source->name;
// absolute file ?
@@ -23,11 +23,11 @@ class Smarty_Internal_Resource_Stream extends Smarty_Resource_Recompiled
* populate Source Object with meta data from Resource
*
* @param Smarty_Template_Source $source source object
* @param Smarty_Internal_Template $_template template object
* @param Smarty_Internal_Template|null $_template template object
*
* @return void
*/
public function populate(Smarty_Template_Source $source, Smarty_Internal_Template $_template = null)
public function populate(Smarty_Template_Source $source, ?Smarty_Internal_Template $_template = null)
{
if (strpos($source->resource, '://') !== false) {
$source->filepath = $source->resource;
@@ -22,11 +22,11 @@ class Smarty_Internal_Resource_String extends Smarty_Resource
* populate Source Object with meta data from Resource
*
* @param Smarty_Template_Source $source source object
* @param Smarty_Internal_Template $_template template object
* @param Smarty_Internal_Template|null $_template template object
*
* @return void
*/
public function populate(Smarty_Template_Source $source, Smarty_Internal_Template $_template = null)
public function populate(Smarty_Template_Source $source, ?Smarty_Internal_Template $_template = null)
{
$source->uid = $source->filepath = sha1($source->name . $source->smarty->_joined_template_dir);
$source->timestamp = $source->exists = true;
@@ -17,11 +17,11 @@ class Smarty_Internal_Runtime_CodeFrame
/**
* Create code frame for compiled and cached templates
*
* @param Smarty_Internal_Template $_template
* @param string $content optional template content
* @param string $functions compiled template function and block code
* @param bool $cache flag for cache file
* @param \Smarty_Internal_TemplateCompilerBase $compiler
* @param Smarty_Internal_Template $_template
* @param string $content optional template content
* @param string $functions compiled template function and block code
* @param bool $cache flag for cache file
* @param \Smarty_Internal_TemplateCompilerBase|null $compiler
*
* @return string
*/
@@ -30,7 +30,7 @@ class Smarty_Internal_Runtime_CodeFrame
$content = '',
$functions = '',
$cache = false,
Smarty_Internal_TemplateCompilerBase $compiler = null
?Smarty_Internal_TemplateCompilerBase $compiler = null
) {
// build property code
$properties[ 'version' ] = Smarty::SMARTY_VERSION;
@@ -168,7 +168,7 @@ class Smarty_Internal_Runtime_Inheritance
public function process(
Smarty_Internal_Template $tpl,
Smarty_Internal_Block $block,
Smarty_Internal_Block $parent = null
?Smarty_Internal_Block $parent = null
) {
if ($block->hide && !isset($block->child)) {
return;
+1 -1
View File
@@ -149,7 +149,7 @@ class Smarty_Internal_Template extends Smarty_Internal_TemplateBase
public function __construct(
$template_resource,
Smarty $smarty,
Smarty_Internal_Data $_parent = null,
?Smarty_Internal_Data $_parent = null,
$_cache_id = null,
$_compile_id = null,
$_caching = null,
@@ -386,7 +386,7 @@ abstract class Smarty_Internal_TemplateCompilerBase
public function compileTemplate(
Smarty_Internal_Template $template,
$nocache = null,
Smarty_Internal_TemplateCompilerBase $parent_compiler = null
?Smarty_Internal_TemplateCompilerBase $parent_compiler = null
) {
// get code frame of compiled template
$_compiled_code = $template->smarty->ext->_codeFrame->create(
@@ -407,9 +407,9 @@ abstract class Smarty_Internal_TemplateCompilerBase
/**
* Compile template source and run optional post filter
*
* @param \Smarty_Internal_Template $template
* @param null|bool $nocache flag if template must be compiled in nocache mode
* @param \Smarty_Internal_TemplateCompilerBase $parent_compiler
* @param \Smarty_Internal_Template $template
* @param null|bool $nocache flag if template must be compiled in nocache mode
* @param \Smarty_Internal_TemplateCompilerBase|null $parent_compiler
*
* @return string
* @throws \Exception
@@ -417,7 +417,7 @@ abstract class Smarty_Internal_TemplateCompilerBase
public function compileTemplateSource(
Smarty_Internal_Template $template,
$nocache = null,
Smarty_Internal_TemplateCompilerBase $parent_compiler = null
?Smarty_Internal_TemplateCompilerBase $parent_compiler = null
) {
try {
// save template object in compiler class
@@ -455,28 +455,28 @@ abstract class Smarty_Internal_TemplateCompilerBase
$this->smarty->_current_file = $this->template->source->filepath;
// get template source
if (!empty($this->template->source->components)) {
$_compiled_code = '<?php $_smarty_tpl->_loadInheritance(); $_smarty_tpl->inheritance->init($_smarty_tpl, true); ?>';
$_compiled_code = '<?php $_smarty_tpl->_loadInheritance(); $_smarty_tpl->inheritance->init($_smarty_tpl, true); ?>';
$i = 0;
$reversed_components = array_reverse($this->template->getSource()->components);
foreach ($reversed_components as $source) {
$i++;
if ($i === count($reversed_components)) {
$_compiled_code .= '<?php $_smarty_tpl->inheritance->endChild($_smarty_tpl); ?>';
}
$_compiled_code .= $this->compileTag(
'include',
[
var_export($source->resource, true),
['scope' => 'parent'],
]
);
}
$_compiled_code = $this->postFilter($_compiled_code, $this->template);
$i = 0;
$reversed_components = array_reverse($this->template->getSource()->components);
foreach ($reversed_components as $source) {
$i++;
if ($i === count($reversed_components)) {
$_compiled_code .= '<?php $_smarty_tpl->inheritance->endChild($_smarty_tpl); ?>';
}
$_compiled_code .= $this->compileTag(
'include',
[
var_export($source->resource, true),
['scope' => 'parent'],
]
);
}
$_compiled_code = $this->postFilter($_compiled_code, $this->template);
} else {
// get template source
$_content = $this->template->source->getContent();
$_compiled_code = $this->postFilter($this->doCompile($this->preFilter($_content), true));
$_compiled_code = $this->postFilter($this->doCompile($this->preFilter($_content), true));
}
if (!empty($this->required_plugins[ 'compiled' ]) || !empty($this->required_plugins[ 'nocache' ])) {
$_compiled_code = '<?php ' . $this->compileRequiredPlugins() . "?>\n" . $_compiled_code;
@@ -617,7 +617,8 @@ abstract class Smarty_Internal_TemplateCompilerBase
{
if (!$this->smarty->security_policy || $this->smarty->security_policy->isTrustedPhpFunction($name, $this)) {
if (strcasecmp($name, 'isset') === 0 || strcasecmp($name, 'empty') === 0
|| strcasecmp($name, 'array') === 0 || is_callable($name)
|| strcasecmp($name, 'array') === 0
|| (is_callable($name) && !isset($this->smarty->registered_plugins[Smarty::PLUGIN_MODIFIER][$name]))
) {
$func_name = smarty_strtolower_ascii($name);
@@ -649,28 +650,42 @@ abstract class Smarty_Internal_TemplateCompilerBase
}
if ($func_name === 'empty') {
return $func_name . '(' .
str_replace("')->value", "',null,true,false)->value", $parameter[ 0 ]) . ')';
str_replace("')->value", "',null,true,false)->value", $parameter[0]) . ')';
} else {
return $func_name . '(' . $parameter[ 0 ] . ')';
return $func_name . '(' . $parameter[0] . ')';
}
} else {
if (
!$this->smarty->loadPlugin('smarty_modifiercompiler_' . $name)
&& !isset($this->smarty->registered_plugins[Smarty::PLUGIN_MODIFIER][$name])
&& !in_array($name, ['time', 'join', 'is_array', 'in_array', 'count'])
) {
trigger_error('Using unregistered function "' . $name . '" in a template is deprecated and will be ' .
'removed in a future release. Use Smarty::registerPlugin to explicitly register ' .
'a custom modifier.', E_USER_DEPRECATED);
}
if (
!$this->smarty->loadPlugin('smarty_modifiercompiler_' . $name)
&& !isset($this->smarty->registered_plugins[Smarty::PLUGIN_MODIFIER][$name])
&& !in_array($name, ['time', 'join', 'is_array', 'in_array', 'count'])
) {
trigger_error('Using unregistered function "' . $name . '" in a template is deprecated and will be ' .
'removed in a future release. Use Smarty::registerPlugin to explicitly register ' .
'a custom modifier.', E_USER_DEPRECATED);
}
return $name . '(' . implode(',', $parameter) . ')';
return $name . '(' . implode(',', $parameter) . ')';
}
} else {
$this->trigger_template_error("unknown function '{$name}'");
}
}
if (isset($this->smarty->registered_plugins[Smarty::PLUGIN_MODIFIER][$name])) {
if ($name === $this->smarty->registered_plugins[Smarty::PLUGIN_MODIFIER][$name][0]) {
return $name . '(' . implode(',', $parameter) . ')';
}
return sprintf(
'call_user_func_array($_smarty_tpl->registered_plugins[ \'%s\' ][ %s ][ 0 ], array( %s ))',
Smarty::PLUGIN_MODIFIER,
var_export($name, true),
implode(',', $parameter)
);
}
$this->trigger_template_error("unknown function '{$name}'");
}
/**
+8 -8
View File
@@ -165,16 +165,16 @@ abstract class Smarty_Resource
* wrapper for backward compatibility to versions < 3.1.22
* Either [$_template] or [$smarty, $template_resource] must be specified
*
* @param Smarty_Internal_Template $_template template object
* @param Smarty $smarty smarty object
* @param string $template_resource resource identifier
* @param Smarty_Internal_Template|null $_template template object
* @param Smarty|null $smarty smarty object
* @param string $template_resource resource identifier
*
* @return \Smarty_Template_Source Source Object
* @throws \SmartyException
*/
public static function source(
Smarty_Internal_Template $_template = null,
Smarty $smarty = null,
?Smarty_Internal_Template $_template = null,
?Smarty $smarty = null,
$template_resource = null
) {
return Smarty_Template_Source::load($_template, $smarty, $template_resource);
@@ -193,10 +193,10 @@ abstract class Smarty_Resource
/**
* populate Source Object with meta data from Resource
*
* @param Smarty_Template_Source $source source object
* @param Smarty_Internal_Template $_template template object
* @param Smarty_Template_Source $source source object
* @param Smarty_Internal_Template|null $_template template object
*/
abstract public function populate(Smarty_Template_Source $source, Smarty_Internal_Template $_template = null);
abstract public function populate(Smarty_Template_Source $source, ?Smarty_Internal_Template $_template = null);
/**
* populate Source Object with timestamp and exists from Resource
+3 -3
View File
@@ -42,10 +42,10 @@ abstract class Smarty_Resource_Custom extends Smarty_Resource
/**
* populate Source Object with meta data from Resource
*
* @param Smarty_Template_Source $source source object
* @param Smarty_Internal_Template $_template template object
* @param Smarty_Template_Source $source source object
* @param Smarty_Internal_Template|null $_template template object
*/
public function populate(Smarty_Template_Source $source, Smarty_Internal_Template $_template = null)
public function populate(Smarty_Template_Source $source, ?Smarty_Internal_Template $_template = null)
{
$source->filepath = $source->type . ':' . $this->generateSafeName($source->name);
$source->uid = sha1($source->type . ':' . $source->name);
+24 -2
View File
@@ -590,12 +590,34 @@ class Smarty_Security
*/
private function _checkDir($filepath, $dirs)
{
$directory = dirname($this->smarty->_realpath($filepath, true)) . DIRECTORY_SEPARATOR;
// Resolve the canonical, symlink-free path of the requested file so that
// a symlink located inside a trusted directory cannot be abused to read
// a file outside of it (CWE-22 path traversal). Smarty::_realpath() only
// normalizes the path as a string and does not follow symlinks, so we
// fall back to it only when the file does not yet exist on disk (e.g.
// config/cache paths that are validated before being written).
$realpath = @realpath($filepath);
$resolved = $realpath !== false ? $realpath : $this->smarty->_realpath($filepath, true);
$directory = dirname($resolved) . DIRECTORY_SEPARATOR;
// Canonicalize the trusted directories the same way. This keeps
// legitimate symlinked deployment paths working (e.g. a Capistrano-style
// "current" release symlink, or macOS' /var -> /private/var): both the
// file and the trusted directories are compared after symlinks have been
// resolved.
$trusted = array();
foreach ($dirs as $dir => $unused) {
$trusted[ $dir ] = true;
if (($dirRealpath = @realpath($dir)) !== false) {
$trusted[ rtrim($dirRealpath, '\\/') . DIRECTORY_SEPARATOR ] = true;
}
}
$_directory = array();
if (!preg_match('#[\\\\/][.][.][\\\\/]#', $directory)) {
while (true) {
// test if the directory is trusted
if (isset($dirs[ $directory ])) {
if (isset($trusted[ $directory ])) {
return $_directory;
}
// abort if we've reached root
+5 -5
View File
@@ -63,16 +63,16 @@ class Smarty_Template_Config extends Smarty_Template_Source
* initialize Source Object for given resource
* Either [$_template] or [$smarty, $template_resource] must be specified
*
* @param Smarty_Internal_Template $_template template object
* @param Smarty $smarty smarty object
* @param string $template_resource resource identifier
* @param Smarty_Internal_Template|null $_template template object
* @param Smarty|null $smarty smarty object
* @param string $template_resource resource identifier
*
* @return Smarty_Template_Config Source Object
* @throws SmartyException
*/
public static function load(
Smarty_Internal_Template $_template = null,
Smarty $smarty = null,
?Smarty_Internal_Template $_template = null,
?Smarty $smarty = null,
$template_resource = null
) {
static $_incompatible_resources = array('extends' => true, 'php' => true);
+5 -5
View File
@@ -148,16 +148,16 @@ class Smarty_Template_Source
* initialize Source Object for given resource
* Either [$_template] or [$smarty, $template_resource] must be specified
*
* @param Smarty_Internal_Template $_template template object
* @param Smarty $smarty smarty object
* @param string $template_resource resource identifier
* @param Smarty_Internal_Template|null $_template template object
* @param Smarty|null $smarty smarty object
* @param string $template_resource resource identifier
*
* @return Smarty_Template_Source Source Object
* @throws SmartyException
*/
public static function load(
Smarty_Internal_Template $_template = null,
Smarty $smarty = null,
?Smarty_Internal_Template $_template = null,
?Smarty $smarty = null,
$template_resource = null
) {
if ($_template) {
+1 -1
View File
@@ -21,7 +21,7 @@ class SmartyCompilerException extends SmartyException
int $code = 0,
?string $filename = null,
?int $line = null,
Throwable $previous = null
?Throwable $previous = null
) {
parent::__construct($message, $code, $previous);
+3 -1
View File
@@ -49,10 +49,12 @@ nav:
- 'Introduction': 'designers/language-modifiers/index.md'
- 'capitalize': 'designers/language-modifiers/language-modifier-capitalize.md'
- 'cat': 'designers/language-modifiers/language-modifier-cat.md'
- 'count': 'designers/language-modifiers/language-modifier-count.md'
- 'count_characters': 'designers/language-modifiers/language-modifier-count-characters.md'
- 'count_paragraphs': 'designers/language-modifiers/language-modifier-count-paragraphs.md'
- 'count_sentences': 'designers/language-modifiers/language-modifier-count-sentences.md'
- 'count_words': 'designers/language-modifiers/language-modifier-count-words.md'
- 'debug_print_var': 'designers/language-modifiers/language-modifier-debug-print-var.md'
- 'date_format': 'designers/language-modifiers/language-modifier-date-format.md'
- 'default': 'designers/language-modifiers/language-modifier-default.md'
- 'escape': 'designers/language-modifiers/language-modifier-escape.md'
@@ -122,4 +124,4 @@ nav:
- 'programmers/caching.md'
- 'programmers/resources.md'
- 'programmers/advanced-features.md'
- 'programmers/plugins.md'
- 'programmers/plugins.md'
+2 -1
View File
@@ -14,4 +14,5 @@ $COMPOSE_CMD run --rm php74 ./run-tests.sh $@ && \
$COMPOSE_CMD run --rm php80 ./run-tests.sh $@ && \
$COMPOSE_CMD run --rm php81 ./run-tests.sh $@ && \
$COMPOSE_CMD run --rm php82 ./run-tests.sh $@ && \
$COMPOSE_CMD run --rm php83 ./run-tests.sh $@
$COMPOSE_CMD run --rm php83 ./run-tests.sh $@ && \
$COMPOSE_CMD run --rm php84 ./run-tests.sh $@
+1 -1
View File
@@ -68,7 +68,7 @@ class PHPUnit_Smarty extends PHPUnit\Framework\TestCase
*/
public static function setUpBeforeClass(): void
{
error_reporting(E_ALL & ~E_STRICT);
error_reporting(E_ALL);
self::$init = true;
self::$pluginsdir =self::getSmartyPluginsDir();
}
@@ -5,10 +5,10 @@ class Smarty_Resource_Filetest extends Smarty_Internal_Resource_File
/**
* populate Source Object with meta data from Resource
*
* @param Smarty_Template_Source $source source object
* @param Smarty_Internal_Template $_template template object
* @param Smarty_Template_Source $source source object
* @param Smarty_Internal_Template|null $_template template object
*/
public function populate(Smarty_Template_Source $source, Smarty_Internal_Template $_template = null)
public function populate(Smarty_Template_Source $source, ?Smarty_Internal_Template $_template = null)
{
parent::populate($source, $_template);
if ($source->exists) {
@@ -12,7 +12,7 @@
class Smarty_Resource_Db4 extends Smarty_Resource
{
public function populate(Smarty_Template_Source $source, Smarty_Internal_Template $_template = null)
public function populate(Smarty_Template_Source $source, ?Smarty_Internal_Template $_template = null)
{
$source->filepath = 'db4:';
$source->uid = sha1($source->resource);
@@ -38,10 +38,10 @@ class Smarty_Resource_Ambiguous extends Smarty_Internal_Resource_File
/**
* populate Source Object with meta data from Resource
*
* @param Smarty_Template_Source $source source object
* @param Smarty_Internal_Template $_template template object
* @param Smarty_Template_Source $source source object
* @param Smarty_Internal_Template|null $_template template object
*/
public function populate(Smarty_Template_Source $source, Smarty_Internal_Template $_template = null)
public function populate(Smarty_Template_Source $source, ?Smarty_Internal_Template $_template = null)
{
$segment = '';
if ($this->segment) {
@@ -12,7 +12,7 @@
class Smarty_Resource_Db extends Smarty_Resource_Recompiled {
public function populate(Smarty_Template_Source $source, Smarty_Internal_Template $_template = null) {
public function populate(Smarty_Template_Source $source, ?Smarty_Internal_Template $_template = null) {
$source->filepath = 'db:';
$source->uid = sha1($source->resource);
$source->timestamp = 1000000000;
@@ -12,7 +12,7 @@
class Smarty_Resource_Db2 extends Smarty_Resource_Recompiled
{
public function populate(Smarty_Template_Source $source, Smarty_Internal_Template $_template = null)
public function populate(Smarty_Template_Source $source, ?Smarty_Internal_Template $_template = null)
{
$source->filepath = 'db2:';
$source->uid = sha1($source->resource);
@@ -12,7 +12,7 @@
class Smarty_Resource_Db3 extends Smarty_Resource
{
public function populate(Smarty_Template_Source $source, Smarty_Internal_Template $_template = null)
public function populate(Smarty_Template_Source $source, ?Smarty_Internal_Template $_template = null)
{
$source->filepath = 'db3:';
$source->uid = sha1($source->resource);
@@ -12,7 +12,7 @@
class Smarty_Resource_Db4 extends Smarty_Resource
{
public function populate(Smarty_Template_Source $source, Smarty_Internal_Template $_template = null)
public function populate(Smarty_Template_Source $source, ?Smarty_Internal_Template $_template = null)
{
$source->filepath = 'db4:';
$source->uid = sha1($source->resource);
+83 -2
View File
@@ -152,10 +152,10 @@ class SecurityTest extends PHPUnit_Smarty
*/
public function testAllowedModifier1()
{
error_reporting(E_ALL & E_STRICT);
error_reporting(E_ALL);
$this->smarty->security_policy->allowed_modifiers = array('capitalize');
$this->assertEquals("Hello World", $this->smarty->fetch('string:{"hello world"|capitalize}'));
error_reporting(E_ALL | E_STRICT);
error_reporting(E_ALL);
}
public function testAllowedModifier2()
@@ -375,6 +375,87 @@ class SecurityTest extends PHPUnit_Smarty
$this->smarty->display('string:{$smarty.template_object}');
}
/**
* A symlink located inside a trusted secure_dir must not be usable to read
* a file outside of it (CWE-22 path traversal via symlink).
*/
public function testSymlinkEscapeFromSecureDirIsRejected()
{
list($secureDir, $outsideFile) = $this->createSymlinkFixture('secret-outside-content');
$link = $secureDir . DIRECTORY_SEPARATOR . 'finance_doc';
if (!@symlink($outsideFile, $link)) {
$this->markTestSkipped('Unable to create symlinks on this platform');
}
$this->smarty->security_policy->secure_dir = array($secureDir . DIRECTORY_SEPARATOR);
$this->expectException('SmartyException');
$this->expectExceptionMessage('not trusted file path');
// Use forward slashes: backslashes in a double-quoted template string are
// interpreted as escape sequences (\f, \r, ...), which would corrupt a
// Windows path. Forward slashes work on every platform.
$this->smarty->fetch('string:{include file="' . str_replace('\\', '/', $link) . '"}');
}
/**
* A symlink that stays inside the trusted secure_dir must keep working, so
* legitimate (e.g. deployment) symlinks are not broken by the fix above.
*/
public function testSymlinkWithinSecureDirIsAllowed()
{
list($secureDir) = $this->createSymlinkFixture('secret-outside-content');
$target = $secureDir . DIRECTORY_SEPARATOR . 'real.tpl';
file_put_contents($target, 'inside-content');
$link = $secureDir . DIRECTORY_SEPARATOR . 'linked.tpl';
if (!@symlink($target, $link)) {
$this->markTestSkipped('Unable to create symlinks on this platform');
}
$this->smarty->security_policy->secure_dir = array($secureDir . DIRECTORY_SEPARATOR);
// Forward slashes so backslashes in a Windows path are not mistaken for
// escape sequences inside the double-quoted template string.
$this->assertEquals('inside-content', $this->smarty->fetch('string:{include file="' . str_replace('\\', '/', $link) . '"}'));
}
/**
* Builds a temporary directory tree for the symlink tests: a (canonicalized)
* secure directory plus a file located outside of it. The tree is removed in
* tearDown(). Returns array(secureDir, outsideFile).
*/
private function createSymlinkFixture($outsideContent)
{
$base = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'smarty_symlink_' . getmypid() . '_' . uniqid();
$secureDir = $base . DIRECTORY_SEPARATOR . 'secure';
mkdir($secureDir, 0777, true);
$outsideFile = $base . DIRECTORY_SEPARATOR . 'outside.txt';
file_put_contents($outsideFile, $outsideContent);
// Canonicalize so secure_dir is symlink-free (sys_get_temp_dir() itself
// may sit under a symlink, e.g. /var -> /private/var on macOS).
$this->symlinkFixtureDir = realpath($base);
return array(realpath($secureDir), realpath($outsideFile));
}
/** @var string|null temp dir created by createSymlinkFixture(), removed in tearDown */
private $symlinkFixtureDir = null;
protected function tearDown(): void
{
if (!empty($this->symlinkFixtureDir) && is_dir($this->symlinkFixtureDir)) {
$it = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($this->symlinkFixtureDir, \FilesystemIterator::SKIP_DOTS),
\RecursiveIteratorIterator::CHILD_FIRST
);
foreach ($it as $entry) {
($entry->isDir() && !$entry->isLink()) ? rmdir($entry->getPathname()) : unlink($entry->getPathname());
}
rmdir($this->symlinkFixtureDir);
$this->symlinkFixtureDir = null;
}
parent::tearDown();
}
}
class mysecuritystaticclass
@@ -0,0 +1,55 @@
<?php
// first class callables where introduced in PHP 8.1
if (PHP_VERSION_ID >= 80100) {
/**
* class for register modifier with (first class) callables tests
*
* @runTestsInSeparateProcess
* @preserveGlobalState disabled
* @backupStaticAttributes enabled
*/
class RegisterModifierFirstClassCallablesTest extends PHPUnit_Smarty
{
public function setUp(): void
{
$this->setUpSmarty(__DIR__);
}
public function testInit()
{
$this->cleanDirs();
}
public function testRegisterFirstClassCallable()
{
$this->smarty->registerPlugin(Smarty::PLUGIN_MODIFIER, 'testmodifier', eval('return strrev(...);'));
$this->assertEquals('mosredna', $this->smarty->fetch('string:{"andersom"|testmodifier}'));
}
public function testRegisterFirstClassCallableSameName()
{
$this->smarty->registerPlugin(Smarty::PLUGIN_MODIFIER, 'mymodifier', eval('return strrev(...);'));
$this->assertEquals('mosredna', $this->smarty->fetch('string:{"andersom"|mymodifier}'));
}
public function testRegisterFirstClassCallableAsFunc()
{
$this->smarty->registerPlugin(Smarty::PLUGIN_MODIFIER, 'kprint_r_out', eval('return strrev(...);'));
$this->smarty->assign('myVar', 'andersom');
$this->assertEquals('mosredna', $this->smarty->fetch('string:{kprint_r_out($myVar)}'));
}
public function testRegisterFirstClassCallableSameNameAsPhpFunc()
{
$this->smarty->registerPlugin(Smarty::PLUGIN_MODIFIER, 'mymodifierfcc', eval('return strrev(...);'));
$this->assertEquals('mosredna', $this->smarty->fetch('string:{mymodifierfcc("andersom")}'));
}
}
}
function mymodifierfcc($a, $b, $c)
{
return "$a function $b $c";
}
@@ -88,6 +88,22 @@ class RegisterModifierTest extends PHPUnit_Smarty
$this->smarty->unregisterPlugin(Smarty::PLUGIN_MODIFIER, 'testmodifier');
$this->assertTrue(isset($this->smarty->registered_plugins[Smarty::PLUGIN_BLOCK]['testmodifier']));
}
public function testRegisterNativePhpFuncAsString()
{
$this->smarty->registerPlugin(Smarty::PLUGIN_MODIFIER, 'strrev', 'strrev');
$this->smarty->assign('myVar', 'andersom');
$this->assertEquals('mosredna', $this->smarty->fetch('string:{strrev($myVar)}'));
}
public function testRegisterNativePhpFuncUnderDifferentName()
{
$this->smarty->registerPlugin(Smarty::PLUGIN_MODIFIER, 'k_xyz_a', 'strrev');
$this->smarty->assign('myVar', 'andersom');
$this->assertEquals('mosredna', $this->smarty->fetch('string:{k_xyz_a($myVar)}'));
}
}
function mymodifier($a, $b, $c)
@@ -82,4 +82,99 @@ class PluginFunctionFetchTest extends PHPUnit_Smarty
$this->smarty->fetch('string:{fetch file="/templates/../etc/passwd"}');
}
/**
* When a security policy is in effect, {fetch} of a remote resource must not
* follow redirects, otherwise an open redirect on a trusted host could be
* used to bypass trusted_uri and reach an internal target (SSRF, CWE-918).
*
* @runInSeparateProcess
* @preserveGlobalState disabled
*/
public function testFetchRemoteDisablesRedirectsUnderSecurity()
{
FetchContextCaptureStreamWrapper::$capturedOptions = null;
stream_wrapper_register('ssrftest', 'FetchContextCaptureStreamWrapper');
try {
$this->smarty->enableSecurity();
$this->smarty->security_policy->trusted_uri[] = '/^ssrftest:\/\/allowed$/';
$result = $this->smarty->fetch('string:{fetch file="ssrftest://allowed/data"}');
$this->assertSame('BODY', $result);
$this->assertIsArray(FetchContextCaptureStreamWrapper::$capturedOptions);
$this->assertArrayHasKey('http', FetchContextCaptureStreamWrapper::$capturedOptions);
$this->assertSame(0, FetchContextCaptureStreamWrapper::$capturedOptions['http']['follow_location']);
$this->assertLessThanOrEqual(1, FetchContextCaptureStreamWrapper::$capturedOptions['http']['max_redirects']);
} finally {
stream_wrapper_unregister('ssrftest');
}
}
/**
* Without a security policy there is no trusted_uri to bypass, so the
* redirect-disabling stream context is not applied (backwards compatible).
*
* @runInSeparateProcess
* @preserveGlobalState disabled
*/
public function testFetchRemoteKeepsDefaultBehaviorWithoutSecurity()
{
FetchContextCaptureStreamWrapper::$capturedOptions = null;
stream_wrapper_register('ssrftest', 'FetchContextCaptureStreamWrapper');
try {
$result = $this->smarty->fetch('string:{fetch file="ssrftest://allowed/data"}');
$this->assertSame('BODY', $result);
$this->assertSame(array(), FetchContextCaptureStreamWrapper::$capturedOptions);
} finally {
stream_wrapper_unregister('ssrftest');
}
}
}
/**
* Minimal custom stream wrapper used by the fetch SSRF tests: it records the
* stream context options that {fetch} passes to file_get_contents() and returns
* a fixed body so the call succeeds without touching the network.
*/
class FetchContextCaptureStreamWrapper
{
/** @var resource|null populated by PHP when a context is passed */
public $context;
/** @var array|null options captured from the context on the last open */
public static $capturedOptions = null;
private $read = false;
public function stream_open($path, $mode, $options, &$opened_path)
{
self::$capturedOptions = isset($this->context) ? stream_context_get_options($this->context) : array();
return true;
}
public function stream_read($count)
{
if ($this->read) {
return '';
}
$this->read = true;
return 'BODY';
}
public function stream_eof()
{
return $this->read;
}
public function stream_stat()
{
return array();
}
public function url_stat($path, $flags)
{
return array();
}
}
@@ -0,0 +1,84 @@
<?php
/**
* Smarty PHPunit tests of the {html_image} function plugin
*
* @package PHPunit
*/
/**
* class for {html_image} tests
*
* @runTestsInSeparateProcess
* @preserveGlobalState disabled
* @backupStaticAttributes enabled
*/
class PluginFunctionHtmlImageTest extends PHPUnit_Smarty
{
public function setUp(): void
{
$this->setUpSmarty(__DIR__);
}
public function testInit()
{
$this->cleanDirs();
}
/**
* Passing both width and height skips the getimagesize() lookup, so no real
* image file is needed to render the tag.
*/
private function render($params)
{
$tpl = $this->smarty->createTemplate('eval:{html_image file=$file width=$width height=$height href=$href path_prefix=$path_prefix}');
$tpl->assign($params + array(
'file' => 'pic.jpg',
'width' => 44,
'height' => 68,
'href' => '',
'path_prefix' => '',
));
return $tpl->fetch();
}
public function testHrefIsEscaped()
{
$result = $this->render(array('href' => '"><script>alert(1)</script>'));
$this->assertStringNotContainsString('<script>', $result);
$this->assertStringContainsString('&lt;script&gt;', $result);
}
public function testWidthIsEscaped()
{
$result = $this->render(array('width' => '44" onload="alert(1)'));
$this->assertStringNotContainsString('onload="', $result);
$this->assertStringContainsString('&quot;', $result);
}
public function testHeightIsEscaped()
{
$result = $this->render(array('height' => '68" onmouseover="alert(1)'));
$this->assertStringNotContainsString('onmouseover="', $result);
}
public function testFileAndPathPrefixAreEscaped()
{
$result = $this->render(array('file' => 'pic.jpg"><script>alert(1)</script>', 'path_prefix' => '"><b>'));
$this->assertStringNotContainsString('<script>', $result);
$this->assertStringNotContainsString('<b>', $result);
}
/**
* Benign values must be unchanged (no breakage, no double-encoding of an
* ampersand already present in a URL).
*/
public function testBenignValuesAreUnchanged()
{
$result = $this->render(array('width' => 44, 'height' => 68, 'href' => 'detail.php?id=1&page=2'));
$this->assertStringContainsString('width="44"', $result);
$this->assertStringContainsString('height="68"', $result);
$this->assertStringContainsString('src="pic.jpg"', $result);
$this->assertStringContainsString('href="detail.php?id=1&amp;page=2"', $result);
$this->assertStringNotContainsString('&amp;amp;', $result);
}
}
@@ -308,6 +308,19 @@ class PluginFunctionHtmlSelectDateTest extends PHPUnit_Smarty
$this->assertEquals($result, $tpl->fetch());
}
/**
* day_size/month_size/year_size are numeric HTML size attributes and are
* cast to int (consistent with html_select_time) so an untrusted value
* cannot break out of size="…" (CWE-79).
*/
public function testSizeIsCastToInt()
{
$tpl = $this->smarty->createTemplate('eval:{html_select_date time=' . $this->now . ' year_size="5\" onfocus=alert(1) x=\""}');
$result = $tpl->fetch();
$this->assertStringContainsString('size="5"', $result);
$this->assertStringNotContainsString('onfocus', $result);
}
public function testFieldOrder()
{
$n = "\n";
@@ -438,7 +438,31 @@ class CompileFunctionTest extends PHPUnit_Smarty
*/
public function testIllegalFunctionName() {
$this->expectException(SmartyCompilerException::class);
$this->smarty->fetch('string:{function name=\'rce(){};echo "hi";function \'}{/function}');
$this->smarty->fetch('string:{function name=\'rce(){};echo "hi";function \'}{/function}');
}
/**
* test shorthand function definition with regular call
*/
public function testShorthand1()
{
$this->assertEquals("gribus", $this->smarty->fetch('shorthand1.tpl'));
}
/**
* test normal function definition with shorthand call
*/
public function testShorthand2()
{
$this->assertEquals("gribus", $this->smarty->fetch('shorthand2.tpl'));
}
/**
* test shorthand function definition with shorthand call
*/
public function testShorthand3()
{
$this->assertEquals("gribus", $this->smarty->fetch('shorthand3.tpl'));
}
}
@@ -0,0 +1,2 @@
{function blah}gribus{/function}
{call name=blah}
@@ -0,0 +1,2 @@
{function name=blah}gribus{/function}
{blah}
@@ -0,0 +1,2 @@
{function blah}gribus{/function}
{blah}
@@ -0,0 +1,33 @@
<?php
/**
* Smarty PHPunit test reproducing issue #1189.
*
* When a parent template is {include}d, then a child template that {extends}
* the parent overrides a {block}, a subsequent {include} of the parent in the
* same render must still show the parent's block content.
*
* The block override from the extending child must not leak into the later
* include of the parent template.
*
* @see https://github.com/smarty-php/smarty/issues/1189
*
* @preserveGlobalState disabled
*/
class IncludeExtendsBlockLeakIssue1189Test extends PHPUnit_Smarty
{
public function setUp(): void
{
$this->setUpSmarty(__DIR__);
}
/**
* Sequence: include parent -> include child(extends parent) -> include parent.
* Expected: PARENT CHILD PARENT
* Bug (#1189): PARENT CHILD CHILD
*/
public function testBlockOverrideDoesNotLeakIntoLaterParentInclude()
{
$result = $this->smarty->fetch('top.tpl');
$this->assertSame('PARENT CHILD PARENT', preg_replace('/\s+/', ' ', trim($result)));
}
}
@@ -0,0 +1,2 @@
{extends file="parent.tpl"}
{block name=message}CHILD{/block}
@@ -0,0 +1 @@
{block name=message}PARENT{/block}
@@ -0,0 +1 @@
{include file="parent.tpl"} {include file="child.tpl"} {include file="parent.tpl"}
+10
View File
@@ -0,0 +1,10 @@
FROM php:8.4-rc-cli-bullseye
## Basic utilities
RUN apt-get update -yqq && apt-get install -y curl apt-utils git zip unzip
## Composer
COPY ./utilities/testrunners/shared/install-composer.sh /root/install-composer.sh
WORKDIR /root
RUN sh ./install-composer.sh
RUN mv ./composer.phar /usr/local/bin/composer