{"id":"GHSA-8x9c-rmqh-456c","summary":"Twig: Sandbox `__toString()` policy bypass via `Traversable` in `join` and `replace` filters","details":"### Description\n\nThis is a residual bypass of CVE-2026-47732 / GHSA-pr2w-4gpj-cpq4 left after the initial fix for unguarded `__toString()` calls. It covers two related coercion points that were not caught by the original patch.\n\n**`Traversable` in `join` and `replace` filters.** `SandboxExtension::ensureToStringAllowed()` recurses into PHP arrays so that a `Stringable` object hidden inside an array argument cannot be string-coerced without consulting the security policy. The recursion stops at PHP arrays: a `Traversable` value passed at the same position is not materialised, so its contents are not policy-checked. `CoreExtension::join()` and `CoreExtension::replace()` later materialise such `Traversable` inputs through `self::toArray()` and feed them to `implode()` / `strtr()`, both of which implicitly call `__toString()` on contained `Stringable` objects. The bypass also reproduces when the container implements both `Stringable` and `Traversable`: the container's own `__toString()` is policy-checked, but the elements yielded by `getIterator()` are not, and the consuming filters still coerce them to string.\n\n**`in` and `not in` operators.** `InBinary` and `NotInBinary` compile to `CoreExtension::inFilter()`, which falls through to PHP's `\u003c=\u003e` operator when comparing a string with a `Stringable` object. PHP coerces the object to string via `__toString()` without the sandbox policy being consulted. Beyond the direct side effect, `in` can also be used as a content-leak oracle: each probe against an attacker-chosen needle leaks one bit of equality, and chained probes can reconstruct the string returned by `__toString()` even when every method is denied. The bypass reproduces with both array and `Traversable` haystacks, and on both operand sides.\n\nA sandboxed template author who is allowed to call `join` / `replace`, or to use the `in` / `not in` operators, can therefore trigger a disallowed `__toString()` method on objects reachable from the render context, even when that method is not on `SecurityPolicy::$allowedMethods`. The bypass reproduces both under global sandbox mode and when sandboxing is enabled through `SourcePolicyInterface`.\n\n### Resolution\n\n`SandboxExtension::ensureToStringAllowed()` now also recurses into `Traversable` operands when sandboxing is active for the current source: each value is materialised once and run through the same array-recursion path, so the policy is consulted before the filter implementation can coerce contained objects to strings. This applies to plain `Traversable` operands as well as to containers that implement both `Stringable` and `Traversable`: the container's own `__toString()` is still policy-checked, and the yielded elements are additionally checked. The materialisation is guarded by `isSandboxed($source)` so that non-sandboxed code paths do not pay the cost or change generator-exhaustion semantics.\n\n`InBinary` and `NotInBinary` now implement `Twig\\Node\\CoercesChildrenToStringInterface` and declare both operands as string-coerced, so `SandboxNodeVisitor` wraps each operand in `CheckToStringNode`. The policy is consulted before `CoreExtension::inFilter()` reaches PHP's `\u003c=\u003e` operator, matching the existing protection on the other comparison binaries (`Equal`, `Less`, `Greater`, `Spaceship`, ...).\n\n### Credits\n\nTwig would like to thank Vincent55 Yang and Fabien Potencier for reporting the issues and Fabien Potencier for providing the fix.","aliases":["CVE-2026-48807"],"modified":"2026-08-24T00:36:54.743162602Z","published":"2026-06-30T18:43:23Z","database_specific":{"severity":"MODERATE","github_reviewed":true,"github_reviewed_at":"2026-06-30T18:43:23Z","nvd_published_at":null,"cwe_ids":["CWE-693","CWE-863"]},"references":[{"type":"WEB","url":"https://github.com/twigphp/Twig/security/advisories/GHSA-8x9c-rmqh-456c"},{"type":"WEB","url":"https://github.com/FriendsOfPHP/security-advisories/blob/master/twig/twig/CVE-2026-48807.yaml"},{"type":"PACKAGE","url":"https://github.com/twigphp/Twig"},{"type":"WEB","url":"https://github.com/twigphp/Twig/releases/tag/v3.27.0"},{"type":"WEB","url":"https://symfony.com/blog/cve-2026-48807-sandbox-tostring-policy-bypass-via-traversable-in-join-replace-and-in-not-in-operators"}],"affected":[{"package":{"name":"twig/twig","ecosystem":"Packagist","purl":"pkg:composer/twig/twig"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"3.27.0"}]}],"versions":["1.3.0","1.4.0","1.5.0","1.5.1","1.6.0","1.6.1","1.6.2","1.6.3","1.6.4","1.6.5","v1.10.0","v1.10.1","v1.10.2","v1.10.3","v1.11.0","v1.11.1","v1.12.0","v1.12.0-RC1","v1.12.1","v1.12.2","v1.12.3","v1.13.0","v1.13.1","v1.13.2","v1.14.0","v1.14.1","v1.14.2","v1.15.0","v1.15.1","v1.16.0","v1.16.1","v1.16.2","v1.16.3","v1.17.0","v1.18.0","v1.18.1","v1.18.2","v1.19.0","v1.20.0","v1.21.0","v1.21.1","v1.21.2","v1.22.0","v1.22.1","v1.22.2","v1.22.3","v1.23.0","v1.23.1","v1.23.2","v1.23.3","v1.24.0","v1.24.1","v1.24.2","v1.25.0","v1.26.0","v1.26.1","v1.27.0","v1.28.0","v1.28.1","v1.28.2","v1.29.0","v1.30.0","v1.31.0","v1.32.0","v1.33.0","v1.33.1","v1.33.2","v1.34.0","v1.34.1","v1.34.2","v1.34.3","v1.34.4","v1.35.0","v1.35.1","v1.35.2","v1.35.3","v1.35.4","v1.36.0","v1.37.0","v1.37.1","v1.38.0","v1.38.1","v1.38.2","v1.38.3","v1.38.4","v1.39.0","v1.39.1","v1.40.0","v1.40.1","v1.41.0","v1.42.0","v1.42.1","v1.42.2","v1.42.3","v1.42.4","v1.42.5","v1.43.0","v1.43.1","v1.44.0","v1.44.1","v1.44.2","v1.44.3","v1.44.4","v1.44.5","v1.44.6","v1.44.7","v1.44.8","v1.7.0","v1.8.0","v1.8.1","v1.8.2","v1.8.3","v1.9.0","v1.9.1","v1.9.2","v2.0.0","v2.1.0","v2.10.0","v2.11.0","v2.11.1","v2.11.2","v2.11.3","v2.12.0","v2.12.1","v2.12.2","v2.12.3","v2.12.4","v2.12.5","v2.13.0","v2.13.1","v2.14.0","v2.14.1","v2.14.10","v2.14.11","v2.14.12","v2.14.13","v2.14.2","v2.14.3","v2.14.4","v2.14.5","v2.14.6","v2.14.7","v2.14.8","v2.14.9","v2.15.0","v2.15.1","v2.15.2","v2.15.3","v2.15.4","v2.15.5","v2.15.6","v2.16.0","v2.16.1","v2.2.0","v2.3.0","v2.3.1","v2.3.2","v2.4.0","v2.4.1","v2.4.2","v2.4.3","v2.4.4","v2.4.5","v2.4.6","v2.4.7","v2.4.8","v2.5.0","v2.6.0","v2.6.1","v2.6.2","v2.7.0","v2.7.1","v2.7.2","v2.7.3","v2.7.4","v2.8.0","v2.8.1","v2.9.0","v3.0.0","v3.0.0-BETA1","v3.0.1","v3.0.2","v3.0.3","v3.0.4","v3.0.5","v3.1.0","v3.1.1","v3.10.0","v3.10.1","v3.10.2","v3.10.3","v3.11.0","v3.11.1","v3.11.2","v3.11.3","v3.12.0","v3.13.0","v3.14.0","v3.14.1","v3.14.2","v3.15.0","v3.16.0","v3.17.0","v3.17.1","v3.18.0","v3.19.0","v3.2.1","v3.20.0","v3.21.0","v3.21.1","v3.22.0","v3.22.1","v3.22.2","v3.23.0","v3.24.0","v3.25.0","v3.26.0","v3.3.0","v3.3.1","v3.3.10","v3.3.2","v3.3.3","v3.3.4","v3.3.5","v3.3.6","v3.3.7","v3.3.8","v3.3.9","v3.4.0","v3.4.1","v3.4.2","v3.4.3","v3.5.0","v3.5.1","v3.6.0","v3.6.1","v3.7.0","v3.7.1","v3.8.0","v3.9.0","v3.9.1","v3.9.2","v3.9.3"],"database_specific":{"last_known_affected_version_range":"\u003c= 3.26.0","source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/06/GHSA-8x9c-rmqh-456c/GHSA-8x9c-rmqh-456c.json"}}],"schema_version":"1.9.0"}