Human
Machine
REPRO-2026-00151 HIGH
Verified
Twig: sandbox bypass via SourcePolicy filter check enables arbitrary PHP callables
twig/twig (composer) May 22, 2026
What's the vulnerability?
Twig's sandbox can be enabled per-template through a
SourcePolicyInterface, rather than enabled globally for every template.
When the sandbox is enabled this way, the runtime security check for the
sort, filter, map and reduce filters fails to consult the current
template's source. As a result the SourcePolicy is never applied to those
four filters.
An attacker who can supply template content can therefore pass an arbitrary
PHP callable to one of these filters — for example system — and have it
executed, escaping the sandbox and achieving code execution.
Root Cause Analysis
# 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.
One Command
Verify with pruva-verify
Run the Pruva CLI to automatically fetch and execute the reproduction script.
pruva-verify REPRO-2026-00151 or
pruva-verify GHSA-2q52-x2ff-qgfr or
pruva-verify CVE-2026-24425 Install:
curl -fsSL https://pruva.dev/install.sh | sh Or Run Manually
1
Download the script
curl -O https://pruva.dev/api/v1/reproductions/REPRO-2026-00151/artifacts/reproduction_steps.sh 2
Make executable
chmod +x reproduction_steps.sh 3
Run the script
./reproduction_steps.sh Run in a VM, container, or disposable environment. This exploits a real vulnerability.
How Pruva Reproduced This
Watch the AI agent's step-by-step process.
Loading session...
Artifacts
No artifacts available