Skip to content

Track taints through Twig attributes, loops, assignments and includes - #390

Merged
danog merged 5 commits into
psalm:5.xfrom
danog:twig-taint-expressions
Oct 5, 2026
Merged

danog merged 5 commits into
psalm:5.xfrom
danog:twig-taint-expressions

Conversation

@danog

@danog danog commented Oct 2, 2026 •

Copy link
Copy Markdown
Member

Builds on #389 (Psalm 7 support), whose two commits come first here.

The Twig taint analysis only saw a variable printed as is ({{ untrusted|raw }}), so most of the ways a template displays its parameters went unnoticed. Each of the following was missed, and is now reported:

  • Attributes, calls and operators. {{ user.name|raw }}, {{ ('Hello ' ~ name)|raw }}, {{ format(value)|raw }}: a printed expression takes the taints of the variables it is made of, through its filters, and an escape/e filter anywhere in it escapes what it applies to.
  • Conditions. {{ (untrusted ? 'a' : 'b')|raw }} and {{ (untrusted == 'x')|raw }} were reported, though only a constant or a boolean is displayed: the condition of a ternary, and the expressions giving a boolean or a number (comparisons, tests, not, arithmetic), don't carry the taints of what they are made of. A branch still does.
  • Loops. {% for item in items %}{{ item|raw }}{% endfor %}: the loop variables take the taints of what is looped over.
  • Assignments. {% set greeting = 'Hello ' ~ name %}: set was only followed when assigning another variable.
  • A variable printed more than once. {{ untrusted|raw }} followed by {{ untrusted }}: each use of a template variable replaced the previous one as the node its taints flow into, so only the last use was connected. Every use now goes through the node of the first one.
  • Includes. {% include 'part.html.twig' %}: an included template is given the including template's variables (those it reads from its context, unless the include is only) and those of with, and what it outputs is part of what the including template outputs. Only includes with a constant template name are followed.
  • Templates the analysis cannot parse. A template using a filter, function or tag of an extension the analysis was not given, or including a template it cannot load, crashed the whole Psalm run. It is now skipped with a warning.
  • Templates parsed as PHP. Psalm parsed every template as PHP before handing it to TemplateFileAnalyzer, and reported parse errors for those containing <?php, such as templates generating PHP code. TemplateFileScanner, configured as the scanner of the .twig extension next to the checker (see the README), skips that.
  • Templates outside of the template root. A template outside of twigRootPath crashed the analysis (the loader only knew the root); it is now named after its path in the project, which is also how other frameworks' renderers can refer to it.

Tests: 13 new scenarios in TwigTaintingWithAnalyzer.feature; the 7 expecting errors, and the one with a template containing <?php, fail without this change. phpunit (45 tests), codecept run -g symfony-common (106 tests) and psalm --find-dead-code --find-unused-psalm-suppress pass.

@danog
danog force-pushed the twig-taint-expressions branch from 115609b to f19facc Compare October 5, 2026 08:44
@danog
danog merged commit 3b547cb into psalm:5.x Oct 5, 2026
1 check 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