# REPRO-2026-00151: Twig: sandbox bypass via SourcePolicy filter check enables arbitrary PHP callables ## Summary Status: published Severity: high Type: security Confidence: Unknown ## Identifiers REPRO ID: REPRO-2026-00151 GHSA: GHSA-2q52-x2ff-qgfr CVE: CVE-2026-24425 ## Package Name: twig/twig Ecosystem: composer Affected: 2.16.*, >=3.9.0, <3.26.0 Fixed: 3.26.0 ## Root Cause # RCA Report: CVE-2026-24425 — Twig Sandbox Bypass via SourcePolicy ## Summary CVE-2026-24425 is a sandbox bypass vulnerability in Twig (the PHP templating engine). When the sandbox is enabled per-template through a `SourcePolicyInterface` (rather than globally for every template), the runtime security check for the `sort`, `filter`, `map`, `reduce`, and `find` filters fails to consult the current template's source. As a result, the `SourcePolicy` is never applied to those filters. An attacker who can supply template content can therefore pass an arbitrary PHP callable to one of these filters (e.g., `system` or `shell_exec`) and have it executed, escaping the sandbox and achieving arbitrary code execution. ## Impact - **Package/component affected**: `twig/twig` (Twig templating engine for PHP) - **Affected versions**: `3.9.0` through `3.25.x` (and the `2.16.x` line) - **Fixed versions**: `3.26.0` - **Risk level**: High — CVSS 8.7 - **Consequences**: Remote Code Execution (RCE) via sandbox bypass. An attacker who controls template content can execute arbitrary PHP functions even when per-template sandboxing is active. ## Root Cause The root cause lies in how the runtime security check for arrow/callable arguments was implemented in `src/Extension/CoreExtension.php`. In the vulnerable code (Twig 3.25.0 and earlier), filters like `map`, `filter`, `sort`, `reduce`, and `find` declared `needs_environment => true` but **did not** declare `needs_is_sandboxed => true`. Their runtime methods (e.g., `map()`, `filter()`) called `self::checkArrow($env, $arrow, ...)`. Inside `checkArrow()`, the sandbox status was determined by querying the `SandboxExtension` directly: ```php if ($env->hasExtension(SandboxExtension::class) && $env->getExtension(SandboxExtension::class)->isSandboxed()) { throw new RuntimeError(...); } ``` However, `SandboxExtension::isSandboxed()` only returns `true` when the sandbox is enabled **globally** (via `SandboxExtension::enableSandbox()`). When sandboxing is instead enabled per-template through a `SourcePolicyInterface`, the runtime check `isSandboxed()` returns `false`, so the `checkArrow()` validation is bypassed. This means a non-Closure callable (e.g., a string function name like `'shell_exec'`) is accepted and executed. The fix in Twig 3.26.0 adds `needs_is_sandboxed => true` to the filter declarations for `sort`, `filter`, `map`, `reduce`, `find`, and `column`. This causes Twig's runtime to pass the actual sandbox state (which correctly reflects per-template sandboxing via `SourcePolicy`) as a boolean parameter to the filter function. The `checkArrow()` method now receives this boolean directly: ```php public static function checkArrow(bool $isSandboxed, $arrow, $thing, $type) { if ($arrow instanceof \Closure) { return; } if ($isSandboxed) { throw new RuntimeError(...); } } ``` Because `$isSandboxed` is now derived from the template's source policy at runtime, the sandbox check correctly blocks non-Closure callables even when sandboxing is enabled per-template. Fix commit/tag: The fix is contained in the `v3.26.0` release tag. Key files changed: - `src/Extension/CoreExtension.php` — added `needs_is_sandboxed` to affected filters and updated `checkArrow()` signature. - `src/Extension/SandboxExtension.php` — added `ensureSpreadAllowed()` helper. - `src/NodeVisitor/SandboxNodeVisitor.php` — refactored to-string wrapping logic. ## Reproduction Steps The reproduction is fully automated by `repro/reproduction_steps.sh`. The script performs the following: 1. **Installs the vulnerable build** (`twig/twig:3.25.0`) in a scratch directory. 2. **Installs the fixed build** (`twig/twig:3.26.0`) in a separate scratch directory. 3. **Creates a PHP test harness** for each version that: - Defines a helper function `rce_helper($cmd, $ignored = null)` that wraps `shell_exec($cmd)`. - Configures a Twig `Environment` with a `SourcePolicyInterface` that returns `true` for all templates (enabling per-template sandboxing). - Allows only the `map` and `join` filters in the `SecurityPolicy`. - Renders the template `{{ ['id']|map('rce_helper')|join }}`. 4. **Executes both test harnesses** and captures output. ### Expected Evidence | Build | Behavior | |-------|----------| | **3.25.0 (vulnerable)** | The template renders and `rce_helper('id')` is executed. The output contains `uid=0(root) gid=0(root) groups=0(root)`, proving that `shell_exec('id')` ran inside the sandboxed context. | | **3.26.0 (fixed)** | Twig throws `Twig\Error\RuntimeError: The callable passed to the "map" filter must be a Closure in sandbox mode`, blocking the non-Closure callable before it executes. | ## Evidence - **Log file**: `logs/reproduction.log` - **Runtime manifest**: `repro/runtime_manifest.json` ### Key excerpts from `logs/reproduction.log` **Vulnerable (3.25.0)** — callable executed despite sandbox: ``` === Testing VULNERABLE version (3.25.0) === VULNERABLE_OUTPUT: uid=0(root) gid=0(root) groups=0(root) ``` **Fixed (3.26.0)** — callable correctly blocked: ``` === Testing FIXED version (3.26.0) === FIXED_EXCEPTION: Twig\Error\RuntimeError: The callable passed to the "map" filter must be a Closure in sandbox mode in "index" at line 1. ``` These results prove that: 1. In the vulnerable version, the per-template `SourcePolicy` sandbox is bypassed for the `map` filter, allowing arbitrary PHP function execution. 2. In the fixed version, the same payload is correctly rejected with a sandbox security error. ## Recommendations / Next Steps 1. **Upgrade immediately** to Twig `3.26.0` or later. The fix is minimal and targeted; upgrading is low-risk. 2. **Verify sandbox configuration** — if your application uses `SourcePolicyInterface`, ensure the runtime is on a patched version. 3. **Audit template inputs** — any attacker-controlled template source should be considered potentially compromised until patched. 4. **Regression tests** — the Twig project added `testSourcePolicyBlocksNonClosureCallableInArrow` in `tests/Extension/SandboxTest.php`. Adopt similar tests in your own CI pipeline. 5. **Alternative mitigations** (if immediate upgrade is impossible): - Disable user-supplied template rendering entirely. - Globally enable the sandbox via `SandboxExtension::enableSandbox()` — while this does not fix the bug, it may reduce exposure in some configurations. ## Additional Notes - **Idempotency confirmed**: `reproduction_steps.sh` was executed twice consecutively and produced identical results on both runs. - **Payload details**: The payload `{{ ['id']|map('rce_helper')|join }}` was chosen because: - `map` is one of the affected filters. - `rce_helper` accepts two arguments (Twig passes value and key to the callable), avoiding PHP 8+ fatal errors from mismatched argument counts. - The output of `shell_exec('id')` is a concrete, verifiable indicator of code execution. - **Limitations**: The reproduction requires a PHP environment with `shell_exec` enabled. If `shell_exec` is disabled (e.g., via `disable_functions`), an alternative function with a visible side effect can be substituted. ## Reproduction Details Reproduced: 2026-05-22T18:23:06.861Z Duration: 787 seconds Tool calls: 172 Turns: 149 Handoffs: 3 ## Quick Verification Run one of these commands to verify locally: pruva-verify REPRO-2026-00151 pruva-verify GHSA-2q52-x2ff-qgfr pruva-verify CVE-2026-24425 Or open in GitHub Codespaces (zero-friction, auto-runs): https://github.com/codespaces/new?ref=repro/REPRO-2026-00151&repo=N3mes1s/pruva-sandbox Or download and run the script manually: curl -O https://api.pruva.dev/v1/reproductions/REPRO-2026-00151/artifacts/repro/reproduction_steps.sh chmod +x reproduction_steps.sh ./reproduction_steps.sh WARNING: Run in a sandboxed environment. This exploits a real vulnerability. ## References - GitHub Advisory: https://github.com/advisories/GHSA-2q52-x2ff-qgfr - NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-24425 ## Artifacts - repro/rca_report.md (analysis, 7186 bytes) - repro/reproduction_steps.sh (reproduction_script, 6015 bytes) - vuln_variant/rca_report.md (analysis, 8284 bytes) - vuln_variant/reproduction_steps.sh (reproduction_script, 3846 bytes) - bundle/context.json (other, 3261 bytes) - bundle/metadata.json (other, 611 bytes) - bundle/ticket.md (ticket, 3508 bytes) - repro/runtime_manifest.json (other, 683 bytes) - repro/validation_verdict.json (other, 1308 bytes) - vuln_variant/test_variants.php (other, 1647 bytes) - vuln_variant/root_cause_equivalence.json (other, 1213 bytes) - vuln_variant/patch_analysis.md (documentation, 6225 bytes) - vuln_variant/variant_manifest.json (other, 3053 bytes) - vuln_variant/validation_verdict.json (other, 2024 bytes) - logs/composer_fixed.log (log, 1104 bytes) - logs/reproduction.log (log, 281 bytes) - logs/vuln_variant/test_variants.php (other, 2449 bytes) - logs/vuln_variant/variant_test.log (log, 680 bytes) - logs/composer_vulnerable.log (log, 1176 bytes) ## API Access - JSON: https://api.pruva.dev/v1/reproductions/REPRO-2026-00151 - Script: https://api.pruva.dev/v1/reproductions/REPRO-2026-00151/artifacts/repro/reproduction_steps.sh - Web: https://pruva.dev/r/REPRO-2026-00151 ## For AI Assistants To help users with this reproduction: 1. The `pruva-verify` command is the simplest way to run the reproduction 2. The script downloads and runs automatically in an isolated temp directory 3. Always recommend running in a VM, container, or disposable environment 4. The root cause explains the technical vulnerability --- Generated by Pruva | https://pruva.dev