Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]
- Security: when auto-escaping is enabled, the `escape` modifier (default `html` mode) was compiled to a no-op, leaving values completely unescaped when combined with `nofilter` (e.g. `{$var|escape|nl2br nofilter}`) (CWE-79). The modifier now always escapes and marks its output as safe, so auto-escaping does not double-escape it; `{$var|escape|nl2br}` now also keeps the `<br />` tags produced by `nl2br` unescaped, and the charset/`double_encode` parameters of an explicit `escape` are honored again [#1188](https://github.com/smarty-php/smarty/issues/1188)
- Security: the internal "safe output" marker set by `|raw` and the escaping modifiers no longer leaks out of tags that do not print anything (e.g. `{if $x|escape:'url'}`, `{assign var=y value=$x|raw}`, `{$x|escape assign=y}`), which used to disable auto-escaping of the next printed variable (CWE-79)
- Documented the `nofilter` tag flag and the `force` escape mode, and their interaction with auto-escaping [#1188](https://github.com/smarty-php/smarty/issues/1188)

## [5.8.4] - 2026-06-29
- 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.
Expand Down
21 changes: 17 additions & 4 deletions docs/api/configuring.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,13 +143,21 @@ Enable auto-escaping for HTML as follows:
$smarty->setEscapeHtml(true);
```

When auto-escaping is enabled, the `|escape` modifier's default mode (`html`) has no effect,
to avoid double-escaping. It is possible to force it with the `force` mode.
When auto-escaping is enabled, applying the
[escape modifier](../designers/language-modifiers/language-modifier-escape.md) explicitly
does not result in double-escaping: the modifier escapes the value, and Smarty then
considers it safe and does not escape it again. It is possible to force a second
escaping round with the `force` mode.
Other modes (`htmlall`, `url`, `urlpathinfo`, `quotes`, `javascript`) may be used
with the result you might expect, without double-escaping.

Even when auto-escaping is enabled, you might want to display the content of a variable without
escaping it. To do so, use the `|raw` modifier.
escaping it. To do so, use the `|raw` modifier, or the `nofilter` tag flag, which disables
auto-escaping (and any variable filter) for the whole tag.

Combining an explicit `|escape` with a modifier that produces HTML gives you escaped
content while preserving the generated markup: `{$myVar|escape|nl2br}` outputs the
escaped value with real `<br />` tags.

Examples (with auto-escaping enabled):
```smarty
Expand All @@ -159,14 +167,19 @@ Examples (with auto-escaping enabled):
{$myVar|escape:'html'}

{* no double-escaping on these statements *}
{$var|escape:'htmlall'}
{$myVar|escape:'htmlall'}
{$myVar|escape:'url'}
{$myVar|escape:'urlpathinfo'}
{$myVar|escape:'quotes'}
{$myVar|escape:'javascript'}

{* no escaping at all *}
{$myVar|raw}
{$myVar nofilter}

{* escaped content, with real <br /> tags *}
{$myVar|escape|nl2br}
{$myVar|escape|nl2br nofilter}

{* force double-escaping *}
{$myVar|escape:'force'}
Expand Down
12 changes: 11 additions & 1 deletion docs/designers/language-modifiers/language-modifier-escape.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,20 @@ its `html`.

| Parameter Position | Type | Required | Possible Values | Default | Description |
|--------------------|---------|----------|----------------------------------------------------------------------------------------------------------------|---------|--------------------------------------------------------------------------------------|
| 1 | string | No | `html`, `htmlall`, `url`, `urlpathinfo`, `quotes`, `hex`, `hexentity`, `javascript`, `mail` | `html` | This is the escape format to use. |
| 1 | string | No | `html`, `htmlall`, `url`, `urlpathinfo`, `quotes`, `hex`, `hexentity`, `javascript`, `mail`, `force` | `html` | This is the escape format to use. |
| 2 | string | No | `ISO-8859-1`, `UTF-8`, and any character set supported by [`htmlentities()`](https://www.php.net/htmlentities) | `UTF-8` | The character set encoding passed to htmlentities() et. al. |
| 3 | boolean | No | FALSE | TRUE | Double encode entities from &amp; to &amp;amp; (applies to `html` and `htmlall` only) |

## Interaction with auto-escaping

When [auto-escaping](../../api/configuring.md#enabling-auto-escaping) is enabled, a value
explicitly escaped with this modifier is considered safe and is not escaped a second time.
This also holds when the `nofilter` tag flag is used, so `{$myVar|escape|nl2br nofilter}`
outputs escaped content with real `<br />` tags.

The `force` format behaves like `html`, except that the value is not marked as safe:
with auto-escaping enabled, the output ends up escaped twice.


## Examples

Expand Down
7 changes: 7 additions & 0 deletions docs/designers/language-modifiers/language-modifier-raw.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,10 @@ Prevents variable escaping when [auto-escaping](../../api/configuring.md#enablin
```smarty
{$myVar|raw}
```

Alternatively, the `nofilter` tag flag disables auto-escaping, as well as any
variable filter, for the whole tag:

```smarty
{$myVar nofilter}
```
9 changes: 5 additions & 4 deletions src/Compile/Modifier/EscapeModifierCompiler.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,11 @@ public function compile($params, \Smarty\Compiler\Template $compiler) {
switch ($esc_type) {
case 'html':
case 'force':
// in case of auto-escaping, and without the 'force' option, no double-escaping
if ($compiler->getSmarty()->escape_html && $esc_type != 'force')
return $params[0];
// otherwise, escape the variable
// unless the 'force' option is used, mark the output as already escaped,
// so auto-escaping does not double-escape it
if ($esc_type != 'force') {
$compiler->setRawOutput(true);
}
return 'htmlspecialchars((string)' . $params[ 0 ] . ', ENT_QUOTES, ' . var_export($char_set, true) . ', ' .
var_export($double_encode, true) . ')';
// no break
Expand Down
4 changes: 3 additions & 1 deletion src/Compile/PrintExpressionCompiler.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,9 @@ public function compile($args, \Smarty\Compiler\Template $compiler, $parameter =
$output = $compiler->compileModifier($parameter['modifierlist'], $output);
}
if (isset($_attr['assign'])) {
// assign output to variable
// assign output to variable; nothing is printed, so discard the raw output
// marker possibly set by a modifier, or it would leak into the next output
$compiler->setRawOutput(false);
return "<?php \$_smarty_tpl->assign({$_attr['assign']},{$output});?>";
} else {
// display value
Expand Down
4 changes: 4 additions & 0 deletions src/Compiler/Template.php
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,10 @@ public function compileTag($tag, $args, $parameter = []) {
$this->prefix_code = [];
$result = $this->compileTag2($tag, $args, $parameter);
$this->prefix_code = array_merge($this->prefix_code, array_pop($this->prefixCodeStack));
// tags handled here do not print expressions themselves, so discard the raw
// output marker possibly set by a modifier compiled as part of this tag
// (e.g. {if $x|escape:'url'}), or it would leak into the next output
$this->setRawOutput(false);
return $result;
}

Expand Down
93 changes: 93 additions & 0 deletions tests/UnitTests/A_Core/AutoEscape/AutoEscapeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -125,4 +125,97 @@ public function testAutoEscapeSpecialEscape4() {
$this->assertEquals("<\\'", $this->smarty->fetch($tpl));
}

/**
* test nofilter disables autoescape
*/
public function testAutoEscapeNofilter() {
$tpl = $this->smarty->createTemplate('eval:{$foo nofilter}');
$tpl->assign('foo', '<a@b.c>');
$this->assertEquals("<a@b.c>", $this->smarty->fetch($tpl));
}

/**
* test autoescape + explicit escape still escapes when nofilter is used
* @group issue1188
*/
public function testAutoEscapeEscapeWithNofilter() {
$tpl = $this->smarty->createTemplate('eval:{$foo|escape nofilter}');
$tpl->assign('foo', '<a@b.c>');
$this->assertEquals("&lt;a@b.c&gt;", $this->smarty->fetch($tpl));
}

/**
* test autoescape + escape followed by an HTML-producing modifier, with nofilter
* @group issue1188
*/
public function testAutoEscapeEscapeNl2brWithNofilter() {
$tpl = $this->smarty->createTemplate('eval:{$foo|escape|nl2br nofilter}');
$tpl->assign('foo', "<a@b.c>\nsecond line");
$this->assertEquals("&lt;a@b.c&gt;<br />\nsecond line", $this->smarty->fetch($tpl));
}

/**
* test autoescape + escape followed by an HTML-producing modifier, without nofilter
* @group issue1188
*/
public function testAutoEscapeEscapeNl2br() {
$tpl = $this->smarty->createTemplate('eval:{$foo|escape|nl2br}');
$tpl->assign('foo', "<a@b.c>\nsecond line");
$this->assertEquals("&lt;a@b.c&gt;<br />\nsecond line", $this->smarty->fetch($tpl));
}

/**
* test autoescape + escape modifier honors the double_encode parameter
* @group issue1188
*/
public function testAutoEscapeEscapeHonorsDoubleEncodeParameter() {
$tpl = $this->smarty->createTemplate('eval:{$foo|escape:\'html\':\'UTF-8\':false}');
$tpl->assign('foo', '&amp; <a@b.c>');
$this->assertEquals("&amp; &lt;a@b.c&gt;", $this->smarty->fetch($tpl));
}

/**
* test that escape used in an assign attribute does not disable
* auto-escaping of the next printed variable
* @group issue1188
*/
public function testRawOutputDoesNotLeakFromAssignAttribute() {
$tpl = $this->smarty->createTemplate('eval:{$foo|escape assign=bar}{$foo}');
$tpl->assign('foo', '<a@b.c>');
$this->assertEquals("&lt;a@b.c&gt;", $this->smarty->fetch($tpl));
}

/**
* test that escape used in an {assign} tag does not disable
* auto-escaping of the next printed variable
* @group issue1188
*/
public function testRawOutputDoesNotLeakFromAssignTag() {
$tpl = $this->smarty->createTemplate('eval:{assign var=bar value=$foo|escape}{$foo}');
$tpl->assign('foo', '<a@b.c>');
$this->assertEquals("&lt;a@b.c&gt;", $this->smarty->fetch($tpl));
}

/**
* test that an escaping modifier used in an {if} condition does not disable
* auto-escaping of the next printed variable
* @group issue1188
*/
public function testRawOutputDoesNotLeakFromIfCondition() {
$tpl = $this->smarty->createTemplate('eval:{if $foo|escape:\'url\'}{/if}{$foo}');
$tpl->assign('foo', '<a@b.c>');
$this->assertEquals("&lt;a@b.c&gt;", $this->smarty->fetch($tpl));
}

/**
* test that the raw modifier used in an {assign} tag does not disable
* auto-escaping of the next printed variable
* @group issue1188
*/
public function testRawOutputDoesNotLeakFromRawInAssignTag() {
$tpl = $this->smarty->createTemplate('eval:{assign var=bar value=$foo|raw}{$foo}');
$tpl->assign('foo', '<a@b.c>');
$this->assertEquals("&lt;a@b.c&gt;", $this->smarty->fetch($tpl));
}

}
Loading