Skip to content

Fix escape modifier being a no-op under auto-escaping, breaking nofilter (#1188)#1204

Open
Amaury wants to merge 1 commit into
smarty-php:masterfrom
Amaury:fix/1188-escape-with-nofilter
Open

Fix escape modifier being a no-op under auto-escaping, breaking nofilter (#1188)#1204
Amaury wants to merge 1 commit into
smarty-php:masterfrom
Amaury:fix/1188-escape-with-nofilter

Conversation

@Amaury

@Amaury Amaury commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Fixes #1188. Implements the approach discussed in the issue and approved by @wisskid.

Problem

Since 5.4.0 (#1030), when auto-escaping is enabled, the escape modifier (default html mode) compiled to a no-op, on the assumption that the outer auto-escaping would escape the value anyway. When that outer escaping does not run (nofilter flag or |raw modifier) nothing escapes the value at all:

{$description|escape|nl2br nofilter}   {* 5.4.0: raw, unescaped output — XSS *}

Fix

escape (html mode) now always emits htmlspecialchars() and marks its output as safe via setRawOutput(true) (the mechanism already used by the htmlall, url, quotes and javascript modes) so auto-escaping skips the already-escaped value. The explicit escape then applies regardless of nofilter. The force mode keeps its forced double-escaping semantics by not setting the marker.

Template (auto-escaping on) 5.4.0 This PR
{$var|escape|nl2br nofilter} raw output = XSS escaped + real <br />
{$var|escape|nl2br} <br /> escaped (broken) escaped + real <br />
{$var|escape} escaped once escaped once (unchanged)
{$var|escape:'html':'UTF-8':false} params silently ignored params honored
{$var|escape:'force'} double-escaped double-escaped (unchanged)

Hardening: the raw output marker no longer leaks between tags

The marker was only reset by the display branch of PrintExpressionCompiler. When a marker-setting modifier was compiled inside a tag that prints nothing, the marker leaked and disabled auto-escaping of the next printed variable:

{if $x|escape:'url'}{/if}{$y}          {* 5.4.0: {$y} printed unescaped *}
{assign var=z value=$x|raw}{$y}        {* 5.4.0: {$y} printed unescaped *}

This pre-existing issue would have widened with this fix (plain |escape now sets the marker), so the marker is now discarded at the end of every compileTag() and in the assign branch of PrintExpressionCompiler. Any residual ordering edge case fails toward over-escaping, never toward raw output.

Known limitation (unchanged, inherent to the single boolean marker, and already the case for |raw and the other escape modes): the marker applies to the whole printed expression, so in {$a|escape|cat:$b} the concatenated $b is not escaped.

Tests

9 new tests in AutoEscapeTest (@group issue1188): the exact case from the report, nofilter alone, escape + nofilter, double_encode parameter, and four marker-leak scenarios. Full suite: 2234 tests, 0 failures.

Docs

  • Restored the nofilter documentation (dropped from the navigation with the Smarty 4 $escape_html page): now in the auto-escaping section of configuring.md, plus mentions on the escape and raw modifier pages.
  • Documented the force mode and the interaction of escape with auto-escaping.
  • Fixed the now-inaccurate "the escape modifier has no effect" wording.

…ter (smarty-php#1188)

When auto-escaping is enabled, the escape modifier (default 'html' mode)
compiled to a no-op to avoid double-escaping. When the outer auto-escaping
was suppressed (nofilter flag or |raw), nothing escaped the value at all,
e.g. {$var|escape|nl2br nofilter} output raw HTML (CWE-79).

The modifier now always emits htmlspecialchars() and marks its output as
safe via setRawOutput(true) - the mechanism already used by the htmlall/
url/quotes/javascript modes - so auto-escaping skips it. This also keeps
nl2br's <br /> tags unescaped in {$var|escape|nl2br}, and honors the
charset/double_encode parameters again. The 'force' mode keeps its
forced double-escaping semantics.

Also harden the raw output marker so it cannot leak out of tags that do
not print anything ({if $x|escape:'url'}, {assign var=y value=$x|raw},
{$x|escape assign=y}): it is now discarded at the end of every tag
compilation and in the assign branch of PrintExpressionCompiler.

Restore the nofilter documentation and document the 'force' escape mode.

Fixes smarty-php#1188
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

escape modifier on variables with nofilter has no effect when auto-escaping is enabled

1 participant