# 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.
