Document new support for any expression as a dynamic mapping key

This commit is contained in:
Fabien Potencier
2026-05-23 08:06:17 +02:00
parent 9ff4101463
commit 635cea4789
4 changed files with 105 additions and 0 deletions
+1
View File
@@ -10,6 +10,7 @@
* Fix sandbox `__toString` bypass via `Traversable` arguments to the `join` and `replace` filters (also covers containers that implement both `Stringable` and `Traversable`)
* Fix sandbox `__toString` bypass via the `in` and `not in` operators
* Prevent a stack overflow in `SandboxExtension::ensureToStringAllowed()` when a self-referencing iterable is passed to a sandboxed template
* Add support for any expression as a dynamic mapping key (attribute access, filters, ...)
* Fix sandbox `__toString` policy bypass via dynamic mapping keys
# 3.26.0 (2026-05-20)
+36
View File
@@ -639,6 +639,42 @@ exist:
{% set key = 'name' %}
{(key): 'Fabien', (1 + 1): 2, ('ci' ~ 'ty'): 'city'}
Any expression is supported as a dynamic key. The result is coerced to
string, so objects implementing ``__toString()`` (PHP ``Stringable``) are
accepted:
.. code-block:: twig
{# attribute access #}
{(user.role): 'allowed'}
{# method call #}
{(user.getRole()): 'allowed'}
{# filter result #}
{(name|upper): 'Fabien'}
{# function call #}
{(slug(title)): post}
{# chained expression #}
{(user.email|lower): 'subscribed'}
{# Stringable object (cast via __toString) #}
{(uuid): 'token'}
.. versionadded:: 3.26.1
Support for arbitrary expressions as dynamic mapping keys
(attribute access, method calls, filter results, function calls,
and any ``Stringable`` object) was added in Twig 3.26.1.
.. note::
Inside a sandbox, the ``__toString()`` coercion goes through the
``SecurityPolicy`` method allowlist, the same way as ``{{ obj }}``
or ``{{ obj|upper }}``.
* ``true`` / ``false``: ``true`` represents the true value, ``false``
represents the false value.
+9
View File
@@ -799,6 +799,15 @@ class SandboxTest extends TestCase
$this->assertEquals(1, FooObject::$called['__toString'], 'Sandbox only calls method once');
}
public function testSandboxAllowsArrayDynamicKeyWhenToStringAllowed()
{
$twig = $this->getEnvironment(true, [], [
'index' => '{% set arr = {(obj): "v", (obj.anotherFooObject): "v2"} %}{{ arr|keys|join(",") }}',
], ['set'], ['join', 'keys'], ['Twig\Tests\Extension\FooObject' => ['__toString', 'getAnotherFooObject']]);
$this->assertSame('foo', $twig->load('index')->render(self::$params));
}
public function testSandboxAllowMethodToStringDisabled()
{
$twig = $this->getEnvironment(false, [], self::$templates);
@@ -0,0 +1,59 @@
--TEST--
Mapping keys can be any expression that evaluates to a scalar or Stringable
--TEMPLATE--
{# context variable holding a Stringable object #}
{{ {(obj): 'a'}|keys|join(',') }}
{# attribute access on an object yielding a Stringable #}
{{ {(holder.stringable): 'a'}|keys|join(',') }}
{# attribute access yielding a string #}
{{ {(holder.name): 'a'}|keys|join(',') }}
{# filter result yielding a Stringable #}
{{ {(obj|raw): 'a'}|keys|join(',') }}
{# filter result yielding a string #}
{{ {('hello'|upper): 'a'}|keys|join(',') }}
{# filter result yielding an integer (keys stay int) #}
{{ {(holder.name|length): 'a'}|keys|first == 4 ? 'ok' : 'ko' }}
{# method call yielding a Stringable #}
{{ {(holder.getStringable()): 'a'}|keys|join(',') }}
{# mixed static and dynamic keys #}
{{ {'static': 's', (obj): 'd', (holder.name): 'n'}|keys|join(',') }}
{# nested mapping with a dynamic Stringable key #}
{{ {'outer': {(obj): 'inner'}} | json_encode | raw }}
--DATA--
class TwigTestStringy implements \Stringable {
public function __construct(private string $v) {}
public function __toString(): string { return $this->v; }
}
class TwigTestHolder {
public string $name = 'attr';
public TwigTestStringy $stringable;
public function __construct() { $this->stringable = new TwigTestStringy('attr_obj'); }
public function getStringable(): TwigTestStringy { return new TwigTestStringy('method_obj'); }
}
return ['obj' => new TwigTestStringy('ctx_obj'), 'holder' => new TwigTestHolder()]
--EXPECT--
ctx_obj
attr_obj
attr
ctx_obj
HELLO
ok
method_obj
static,ctx_obj,attr
{"outer":{"ctx_obj":"inner"}}