Improvement of auto-escaping (#1030)

* Evolution of auto-escaping: no double-escaping when using the 'escape' modifier; add the 'force' mode to the 'escape' modifier; add the 'raw' modifier.
* Add 'raw' modifier's documentation
---------

Co-authored-by: Simon Wisselink <s.wisselink@iwink.nl>
This commit is contained in:
Amaury Bouchard
2024-06-30 13:25:30 +02:00
committed by GitHub
parent 3cb3585432
commit 2289fa69f1
12 changed files with 165 additions and 6 deletions

1
changelog/1030.md Normal file
View File

@ -0,0 +1 @@
- Improvement of auto-escaping [#1030](https://github.com/smarty-php/smarty/pull/1030)

View File

@ -143,6 +143,35 @@ Enable auto-escaping for HTML as follows:
$smarty->setEscapeHtml(true); $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.
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.
Examples (with auto-escaping enabled):
```smarty
{* these three statements are identical *}
{$myVar}
{$myVar|escape}
{$myVar|escape:'html'}
{* no double-escaping on these statements *}
{$var|escape:'htmlall'}
{$myVar|escape:'url'}
{$myVar|escape:'urlpathinfo'}
{$myVar|escape:'quotes'}
{$myVar|escape:'javascript'}
{* no escaping at all *}
{$myVar|raw}
{* force double-escaping *}
{$myVar|escape:'force'}
```
## Disabling compile check ## Disabling compile check
By default, Smarty tests to see if the By default, Smarty tests to see if the
current template has changed since the last time current template has changed since the last time

View File

@ -73,6 +73,6 @@ This snippet is useful for emails, but see also
<a href="mailto:{$EmailAddress|escape:'hex'}">{$EmailAddress|escape:'mail'}</a> <a href="mailto:{$EmailAddress|escape:'hex'}">{$EmailAddress|escape:'mail'}</a>
``` ```
See also [escaping smarty parsing](../language-basic-syntax/language-escaping.md), See also [auto-escaping](../../api/configuring.md#enabling-auto-escaping), [escaping smarty parsing](../language-basic-syntax/language-escaping.md),
[`{mailto}`](../language-custom-functions/language-function-mailto.md) and the [obfuscating email [`{mailto}`](../language-custom-functions/language-function-mailto.md) and the [obfuscating email
addresses](../../appendixes/tips.md#obfuscating-e-mail-addresses) page. addresses](../../appendixes/tips.md#obfuscating-e-mail-addresses) pages.

View File

@ -0,0 +1,8 @@
# raw
Prevents variable escaping when [auto-escaping](../../api/configuring.md#enabling-auto-escaping) is activated.
## Basic usage
```smarty
{$myVar|raw}
```

View File

@ -68,6 +68,7 @@ nav:
- 'noprint': 'designers/language-modifiers/language-modifier-noprint.md' - 'noprint': 'designers/language-modifiers/language-modifier-noprint.md'
- 'number_format': 'designers/language-modifiers/language-modifier-number-format.md' - 'number_format': 'designers/language-modifiers/language-modifier-number-format.md'
- 'nl2br': 'designers/language-modifiers/language-modifier-nl2br.md' - 'nl2br': 'designers/language-modifiers/language-modifier-nl2br.md'
- 'raw': 'designers/language-modifiers/language-modifier-raw.md'
- 'regex_replace': 'designers/language-modifiers/language-modifier-regex-replace.md' - 'regex_replace': 'designers/language-modifiers/language-modifier-regex-replace.md'
- 'replace': 'designers/language-modifiers/language-modifier-replace.md' - 'replace': 'designers/language-modifiers/language-modifier-replace.md'
- 'round': 'designers/language-modifiers/language-modifier-round.md' - 'round': 'designers/language-modifiers/language-modifier-round.md'

View File

@ -24,22 +24,32 @@ class EscapeModifierCompiler extends Base {
} }
switch ($esc_type) { switch ($esc_type) {
case 'html': 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
return 'htmlspecialchars((string)' . $params[ 0 ] . ', ENT_QUOTES, ' . var_export($char_set, true) . ', ' . return 'htmlspecialchars((string)' . $params[ 0 ] . ', ENT_QUOTES, ' . var_export($char_set, true) . ', ' .
var_export($double_encode, true) . ')'; var_export($double_encode, true) . ')';
// no break // no break
case 'htmlall': case 'htmlall':
$compiler->setRawOutput(true);
return 'htmlentities(mb_convert_encoding((string)' . $params[ 0 ] . ', \'UTF-8\', ' . return 'htmlentities(mb_convert_encoding((string)' . $params[ 0 ] . ', \'UTF-8\', ' .
var_export($char_set, true) . '), ENT_QUOTES, \'UTF-8\', ' . var_export($char_set, true) . '), ENT_QUOTES, \'UTF-8\', ' .
var_export($double_encode, true) . ')'; var_export($double_encode, true) . ')';
// no break // no break
case 'url': case 'url':
$compiler->setRawOutput(true);
return 'rawurlencode((string)' . $params[ 0 ] . ')'; return 'rawurlencode((string)' . $params[ 0 ] . ')';
case 'urlpathinfo': case 'urlpathinfo':
$compiler->setRawOutput(true);
return 'str_replace("%2F", "/", rawurlencode((string)' . $params[ 0 ] . '))'; return 'str_replace("%2F", "/", rawurlencode((string)' . $params[ 0 ] . '))';
case 'quotes': case 'quotes':
$compiler->setRawOutput(true);
// escape unescaped single quotes // escape unescaped single quotes
return 'preg_replace("%(?<!\\\\\\\\)\'%", "\\\'", (string)' . $params[ 0 ] . ')'; return 'preg_replace("%(?<!\\\\\\\\)\'%", "\\\'", (string)' . $params[ 0 ] . ')';
case 'javascript': case 'javascript':
$compiler->setRawOutput(true);
// escape quotes and backslashes, newlines, etc. // escape quotes and backslashes, newlines, etc.
// see https://html.spec.whatwg.org/multipage/scripting.html#restrictions-for-contents-of-script-elements // see https://html.spec.whatwg.org/multipage/scripting.html#restrictions-for-contents-of-script-elements
return 'strtr((string)' . return 'strtr((string)' .
@ -53,4 +63,4 @@ class EscapeModifierCompiler extends Base {
} }
return '$_smarty_tpl->getSmarty()->getModifierCallback(\'escape\')(' . join(', ', $params) . ')'; return '$_smarty_tpl->getSmarty()->getModifierCallback(\'escape\')(' . join(', ', $params) . ')';
} }
} }

View File

@ -0,0 +1,21 @@
<?php
namespace Smarty\Compile\Modifier;
use Smarty\Exception;
/**
* Smarty raw modifier plugin
* Type: modifier
* Name: raw
* Purpose: when escaping is enabled by default, generates a raw output of a variable
*
* @author Amaury Bouchard
*/
class RawModifierCompiler extends Base {
public function compile($params, \Smarty\Compiler\Template $compiler) {
$compiler->setRawOutput(true);
return ($params[0]);
}
}

View File

@ -75,7 +75,7 @@ class ModifierCompiler extends Base {
} }
} }
} }
return $output; return (string)$output;
} }
/** /**

View File

@ -82,12 +82,13 @@ class PrintExpressionCompiler extends Base {
$output = $compiler->compileModifier($modifierlist, $output); $output = $compiler->compileModifier($modifierlist, $output);
} }
if ($compiler->getTemplate()->getSmarty()->escape_html) { if ($compiler->getTemplate()->getSmarty()->escape_html && !$compiler->isRawOutput()) {
$output = "htmlspecialchars((string) ({$output}), ENT_QUOTES, '" . addslashes(\Smarty\Smarty::$_CHARSET) . "')"; $output = "htmlspecialchars((string) ({$output}), ENT_QUOTES, '" . addslashes(\Smarty\Smarty::$_CHARSET) . "')";
} }
} }
$output = "<?php echo {$output};?>\n"; $output = "<?php echo {$output};?>\n";
$compiler->setRawOutput(false);
} }
return $output; return $output;
} }

View File

@ -313,6 +313,12 @@ class Template extends BaseCompiler {
*/ */
private $noCacheStackDepth = 0; private $noCacheStackDepth = 0;
/**
* disabled auto-escape (when set to true, the next variable output is not auto-escaped)
*
* @var boolean
*/
private $raw_output = false;
/** /**
* Initialize compiler * Initialize compiler
@ -1486,4 +1492,21 @@ class Template extends BaseCompiler {
public function getTagStack(): array { public function getTagStack(): array {
return $this->_tag_stack; return $this->_tag_stack;
} }
/**
* Should the next variable output be raw (true) or auto-escaped (false)
* @return bool
*/
public function isRawOutput(): bool {
return $this->raw_output;
}
/**
* Should the next variable output be raw (true) or auto-escaped (false)
* @param bool $raw_output
* @return void
*/
public function setRawOutput(bool $raw_output): void {
$this->raw_output = $raw_output;
}
} }

View File

@ -35,6 +35,7 @@ class DefaultExtension extends Base {
case 'lower': $this->modifiers[$modifier] = new \Smarty\Compile\Modifier\LowerModifierCompiler(); break; case 'lower': $this->modifiers[$modifier] = new \Smarty\Compile\Modifier\LowerModifierCompiler(); break;
case 'nl2br': $this->modifiers[$modifier] = new \Smarty\Compile\Modifier\Nl2brModifierCompiler(); break; case 'nl2br': $this->modifiers[$modifier] = new \Smarty\Compile\Modifier\Nl2brModifierCompiler(); break;
case 'noprint': $this->modifiers[$modifier] = new \Smarty\Compile\Modifier\NoPrintModifierCompiler(); break; case 'noprint': $this->modifiers[$modifier] = new \Smarty\Compile\Modifier\NoPrintModifierCompiler(); break;
case 'raw': $this->modifiers[$modifier] = new \Smarty\Compile\Modifier\RawModifierCompiler(); break;
case 'round': $this->modifiers[$modifier] = new \Smarty\Compile\Modifier\RoundModifierCompiler(); break; case 'round': $this->modifiers[$modifier] = new \Smarty\Compile\Modifier\RoundModifierCompiler(); break;
case 'str_repeat': $this->modifiers[$modifier] = new \Smarty\Compile\Modifier\StrRepeatModifierCompiler(); break; case 'str_repeat': $this->modifiers[$modifier] = new \Smarty\Compile\Modifier\StrRepeatModifierCompiler(); break;
case 'string_format': $this->modifiers[$modifier] = new \Smarty\Compile\Modifier\StringFormatModifierCompiler(); break; case 'string_format': $this->modifiers[$modifier] = new \Smarty\Compile\Modifier\StringFormatModifierCompiler(); break;
@ -753,4 +754,4 @@ class DefaultExtension extends Base {
return $string; return $string;
} }
} }

View File

@ -61,4 +61,68 @@ class AutoEscapeTest extends PHPUnit_Smarty
$this->assertEquals("<p>hi</p>", $this->smarty->fetch($tpl)); $this->assertEquals("<p>hi</p>", $this->smarty->fetch($tpl));
} }
/**
* test autoescape + raw modifier
*/
public function testAutoEscapeRaw() {
$tpl = $this->smarty->createTemplate('eval:{$foo|raw}');
$tpl->assign('foo', '<a@b.c>');
$this->assertEquals("<a@b.c>", $this->smarty->fetch($tpl));
}
/**
* test autoescape + escape modifier = no double-escaping
*/
public function testAutoEscapeNoDoubleEscape() {
$tpl = $this->smarty->createTemplate('eval:{$foo|escape}');
$tpl->assign('foo', '<a@b.c>');
$this->assertEquals("&lt;a@b.c&gt;", $this->smarty->fetch($tpl));
}
/**
* test autoescape + escape modifier = force double-escaping
*/
public function testAutoEscapeForceDoubleEscape() {
$tpl = $this->smarty->createTemplate('eval:{$foo|escape:\'force\'}');
$tpl->assign('foo', '<a@b.c>');
$this->assertEquals("&amp;lt;a@b.c&amp;gt;", $this->smarty->fetch($tpl));
}
/**
* test autoescape + escape modifier = special escape
*/
public function testAutoEscapeSpecialEscape() {
$tpl = $this->smarty->createTemplate('eval:{$foo|escape:\'url\'}');
$tpl->assign('foo', 'aa bb');
$this->assertEquals("aa%20bb", $this->smarty->fetch($tpl));
}
/**
* test autoescape + escape modifier = special escape
*/
public function testAutoEscapeSpecialEscape2() {
$tpl = $this->smarty->createTemplate('eval:{$foo|escape:\'url\'}');
$tpl->assign('foo', '<BR>');
$this->assertEquals("%3CBR%3E", $this->smarty->fetch($tpl));
}
/**
* test autoescape + escape modifier = special escape
*/
public function testAutoEscapeSpecialEscape3() {
$tpl = $this->smarty->createTemplate('eval:{$foo|escape:\'htmlall\'}');
$tpl->assign('foo', '<BR>');
$this->assertEquals("&lt;BR&gt;", $this->smarty->fetch($tpl));
}
/**
* test autoescape + escape modifier = special escape
*/
public function testAutoEscapeSpecialEscape4() {
$tpl = $this->smarty->createTemplate('eval:{$foo|escape:\'javascript\'}');
$tpl->assign('foo', '<\'');
$this->assertEquals("<\\'", $this->smarty->fetch($tpl));
}
} }