GHSA-vqxf-r9ph-cc9c
HIGHCraft CMS vulnerable to Remote Code Execution via unrestricted file extension
EPSS Exploitation Probability
EPSS (Exploit Prediction Scoring System) is a daily probability model maintained by FIRST.org. It estimates the likelihood a CVE will be exploited in production environments within the next 30 days, derived from real-world threat intelligence signals.
Blast Radius
craftcms/cmsReal-time download stats are indexed for npm and PyPI packages. This vulnerability affects Packagist packages — download data is not available via public APIs for these ecosystems.
Description
Summary
Unrestricted file extension lead to a potential Remote Code Execution (Authenticated, ALLOW_ADMIN_CHANGES=true)
Details
Vulnerability Cause :
If the name parameter value is not empty string('') in the View.php's doesTemplateExist() -> resolveTemplate() -> _resolveTemplateInternal() -> _resolveTemplate() function, it returns directly without extension verification, so that arbitrary extension files are rendered as twig templates (even if they are not extensions set in defaultTemplateExtensions = ['html', 'twig'])
/**
* Searches for a template files, and returns the first match if there is one.
*
* @param string $basePath The base path to be looking in.
* @param string $name The name of the template to be looking for.
* @param bool $publicOnly Whether to only look for public templates (template paths that don’t start with the private template trigger).
* @return string|null The matching file path, or `null`.
*/
private function _resolveTemplate(string $basePath, string $name, bool $publicOnly): ?string
{
// Normalize the path and name
$basePath = FileHelper::normalizePath($basePath);
$name = trim(FileHelper::normalizePath($name), '/');
// $name could be an empty string (e.g. to load the homepage template)
if ($name !== '') {
if ($publicOnly && preg_match(sprintf('/(^|\/)%s/', preg_quote($this->_privateTemplateTrigger, '/')), $name)) {
return null;
}
// Maybe $name is already the full file path
$testPath = $basePath . DIRECTORY_SEPARATOR . $name;
if (is_file($testPath)) {
return $testPath;
}
foreach ($this->_defaultTemplateExtensions as $extension) {
$testPath = $basePath . DIRECTORY_SEPARATOR . $name . '.' . $extension;
if (is_file($testPath)) {
return $testPath;
}
}
}
foreach ($this->_indexTemplateFilenames as $filename) {
foreach ($this->_defaultTemplateExtensions as $extension) {
$testPath = $basePath . ($name !== '' ? DIRECTORY_SEPARATOR . $name : '') . DIRECTORY_SEPARATOR . $filename . '.' . $extension;
if (is_file($testPath)) {
return $testPath;
}
}
}
return null;
}
When attacker with admin privileges on the DEV or Misconfigured STG, PROD, they can exploit this vulnerability to remote code execution (ALLOW_ADMIN_CHANGES=true)
PoC
Step 1) Create a new filesystem. Base Path: /var/www/html/templates

Step 2) Create a new asset volume. Asset Filesystem: template

Step 3) Upload poc file( .txt , .js , .json , etc ) with twig template rce payload
{{'<pre>'}}
{{1337*1337}}
{{['cat /etc/passwd']|map('passthru')|join}}
{{['id;pwd;ls -altr /']|map('passthru')|join}}

Step 4) Create a new global set with template layout. The template filename is poc.js

Step 5) When access global menu or /admin/global/test, poc.js is rendered as a template file and RCE confirmed

Step 6) RCE can be confirmed on other menus(Entries, Categories) where the template file is loaded.

Poc Environment) ALLOW_ADMIN_CHANGES=true, defaultTemplateExtensions=['html','twig']

Impact
Take control of vulnerable systems, Data exfiltrations, Malware execution, Pivoting, etc.
Additionally, there are 371 domains using CraftCMS exposed on Shodan, and among them, 33 servers have "stage" or "dev" included in their hostnames.
although the vulnerability is exploitable only in the authenticated users, configuration with ALLOW_ADMIN_CHANGES=true, there is still a potential security threat (Remote Code Execution)

Remediation
Recommend taking measures by referring to https://github.com/craftcms/cms-ghsa-9f84-5wpf-3vcf/pull/1
// Maybe $name is already the full file path
$testPath = $basePath . DIRECTORY_SEPARATOR . $name;
if (is_file($testPath)) {
// Remedation: Verify template file extension, before return
$fileExt = pathinfo($testPath, PATHINFO_EXTENSION);
$isDisallowed = false;
if (isset($fileExt)) {
$isDisallowed = !in_array($fileExt, $this->_defaultTemplateExtensions);
if($isDisallowed) {
return null;
} else {
return $testPath;
}
}
}

Affected Packages
| Ecosystem | Package | Vulnerable range | Fix |
|---|---|---|---|
| 🐘Packagist | craftcms/cms | ≥ 4.0.0&&< 4.4.6 | 4.4.6 |
Research use only. For defensive security, authorized penetration testing, and academic research only. Never execute exploit code against systems without explicit written authorization.
Detection & mitigation playbook
Open-source dependencyDetect
Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for craftcms/cms. O3's reachability analysis confirms whether the vulnerable code path is actually invoked in your application, so you act on real exposure instead of every transitive match.
Fix
Update craftcms/cms to 4.4.6 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms GHSA-vqxf-r9ph-cc9c is resolved across your whole dependency graph.
Workarounds
If you can't upgrade right away: gate or disable the affected feature, validate untrusted input at the boundary, and avoid passing attacker-controlled data into the vulnerable path. O3's runtime protection blocks exploitation in production as an interim safeguard until the upgrade lands.
How O3 protects you
O3 pinpoints whether GHSA-vqxf-r9ph-cc9c is reachable in your code and exactly where to fix it, then blocks exploitation in production at runtime until the patched version is deployed.
Tailored to GHSA-vqxf-r9ph-cc9c. Runtime protection reduces exposure until a permanent patch is applied and verified — it complements patching, it doesn't replace it.
Frequently Asked Questions
Is GHSA-vqxf-r9ph-cc9c in your dependencies?
O3 detects GHSA-vqxf-r9ph-cc9c across Packagist dependencies and uses function-level reachability to confirm whether the vulnerable code path is actually reachable — not just present. No false positives.