Skip to content

Template: same escaping in partials, isolated scope, validated names - #12

Merged
daedeloth merged 1 commit into
masterfrom
fix/template-hardening
Oct 7, 2026
Merged

daedeloth merged 1 commit into
masterfrom
fix/template-hardening

Conversation

@daedeloth

Copy link
Copy Markdown
Member

What changes

All in Neuron\Core\Template.

Partials now receive the same escaping as pages. template() and combine() build their variables the same way parse() does: a string set with set() or Template::share() is escaped, a value set with setRaw() is left alone, and the original string is available as $_name_. Non-string values (objects, arrays, numbers) are untouched, as before.

Parameters handed to a partial ($this->template ('x.phpt', [ 'name' => $value ])) are passed on as given. They are values of the calling template, whose own variables are already escaped, so escaping them again would encode them twice.

Escaping is explicit. htmlentities ($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') instead of the default flags, so the output is the same on every supported PHP version. On PHP 8.1 and later this is what the defaults already were; on 7.4 and 8.0 single quotes are now encoded as ' and a string with invalid UTF-8 gets a substitution character instead of becoming empty.

Template variables can no longer shadow engine internals. Templates are included from a method that declares no parameters and no locals, so the template's scope holds the template variables and nothing else. $this is still the Template, so the helpers (template(), combine(), layout(), gettext(), ...) and closures defined in a template keep working. A variable named this is dropped.

Template names are validated. A name that is absolute (leading slash or backslash, or a drive letter), has a .. segment, or contains a NUL byte is treated as not found, in the one function every lookup goes through (parse(), template(), combine(), hasTemplate()). The "Template not found" message escapes the name.

Compatibility

  • A partial that escapes a template variable itself (because it used to get it unescaped) now escapes it twice. Print the variable as is, or apply your own escaping to $_name_.
  • A partial that relied on a string variable containing markup needs setRaw() for that variable, as a page already does.
  • A partial that compares a string variable or uses it as a lookup key sees the escaped string, as a page already does. Use $_name_ for the original.
  • Files rendered by a single combine() call no longer share a variable scope: a variable assigned in the first file is not visible in the next.
  • The engine's own variables ($template, $parameters, $ctlbtmpltfiles, ...) are no longer visible inside templates.
  • Variable names that are not valid PHP identifiers (for example a numeric key) are not created; they were reachable only through ${'...'}.
  • Template names with a leading slash used to resolve inside the template folder; they are now not found.

Tests

tests/TemplateHardeningTest.php, written first and seen failing against the old engine. Each escaping case runs three times: as a page, as a template() partial and as a combine() partial.

The full suite gives the same three errors as master (MySQLErrorTest, on a PHP without mysqli) and nothing else. Also smoke-tested on PHP 7.4.

Overlap with #8

#8 wraps the three include statements in try/catch; this PR replaces those same statements with $this->includeTemplate (...). Whichever is merged second gets three small conflicts in Template.php: keep the try/catch from #8 and use the includeTemplate() call from this PR inside it (and drop the variable loops this PR removes).

🤖 Generated with Claude Code

https://claude.ai/code/session_01DqZUi2UiBJP6rZoCSRcdpE

Three changes to how Template renders:

- Partials loaded with template() or combine() now receive the shared and
  template variables exactly like a page does in parse(): strings are
  escaped unless they were set with setRaw(), and the original string is
  available as $_name_. Parameters handed to a partial are passed on as
  given, because they are values of the calling template, whose own
  variables are already escaped.
- Escaping is explicit: htmlentities with ENT_QUOTES | ENT_SUBSTITUTE and
  UTF-8, so the result no longer depends on the PHP version (before 8.1 the
  default flags left single quotes alone and dropped strings with invalid
  UTF-8).
- Templates are included from a method that has no parameters or locals of
  its own, so template variables can no longer shadow the variables the
  engine uses. A variable named "this" is dropped.
- Template names are validated in the one place that looks them up: a name
  that is absolute, has a parent directory segment or contains a NUL byte
  is treated as not found. The "template not found" message escapes the
  name.

Compatibility: a partial that escaped a template variable itself now
escapes it twice and should print the variable as is (or use $_name_).
Files rendered by one combine() call no longer share a variable scope.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DqZUi2UiBJP6rZoCSRcdpE
@daedeloth
daedeloth merged commit 446f9ed into master Oct 7, 2026
5 checks passed
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.

1 participant