SandboxNodeVisitor enforces SecurityPolicy::checkMethodAllowed() for implicit __toString() calls by wrapping selected AST nodes in CheckToStringNode. The set of wrapped nodes is incomplete, and several Twig language constructs still trigger PHP string coercion on a Stringable operand without first consulting the policy. A sandboxed template author can therefore invoke __toString() on any object reachable in the render context, even when __toString on its class is not allowlisted.
Confirmed bypass vectors:
a ? b : c, a ?: b, a ?? b) used as the input of a string-coercing filter or as a filter/function argument.matches operator and the loose comparison operators (==, !=, <, >, <=, >=, <=>), which coerce a Stringable operand to string and can be used as an oracle to recover the value byte by byte (no tag, filter or function needs to be allowlisted).is empty which casts a Stringable value via (string) $value in CoreExtension::testEmpty().Stringable object.include, extends, use, ...), dynamic attribute/property names, and spread arguments from Traversable objects.do tag and the .. range operator.The sandbox now wraps every child node that the parent will string-coerce at runtime, instead of relying on a hardcoded list of node types in SandboxNodeVisitor. A new Twig\Node\CoercesChildrenToStringInterface lets nodes declare which of their children must be guarded; core nodes (concatenation, comparison and range binaries, filter/function/test expressions, do, include, extends, use, ...) implement it. Spread arguments are materialised and policy-checked via the new SandboxExtension::ensureSpreadAllowed(), and dynamic attribute names are checked at runtime inside CoreExtension::getAttribute().
Twig would like to thank Anthropic Glasswing and El Kharoubi Iosif for reporting the issues, and Fabien Potencier for providing the fixes.
{
"cwe_ids": [
"CWE-20"
],
"github_reviewed": true,
"github_reviewed_at": "2026-06-05T21:47:11Z",
"nvd_published_at": null,
"severity": "HIGH"
}