Aug 7, 2026·8 min read·1 visit
Craft CMS local storage driver validates path containment before normalizing the path. This allows attackers to bypass boundary checks using protocol-prefixed traversal sequences, resulting in unauthorized local file access.
A path traversal vulnerability exists in the local filesystem driver of Craft CMS. Due to validation occurring before path normalization, directory containment checks can be bypassed by utilizing specific protocol schemes like 'file://' along with directory traversal sequences. This allows authenticated users with administrative privileges to access or manipulate files outside the defined storage root directory.
The local filesystem driver (Local.php) in Craft CMS is responsible for managing the physical storage and manipulation of assets within configured local storage volumes. It exposes an attack surface through asset management capabilities, where users can upload, index, or rename physical resources. To maintain security boundaries, Craft CMS enforces a directory containment validation mechanism to prevent users from referencing files outside of their designated storage root.
This vulnerability is characterized as an improper limitation of a pathname to a restricted directory, corresponding to CWE-22 and CWE-23. The flaw occurs because of a logical desynchronization between how paths are validated and how they are subsequently normalized before execution. This order-of-operations vulnerability allows directory containment validation to be bypassed completely when specific inputs are processed.
An attacker with administrative privileges or permission to modify asset configurations can submit paths that contain URI protocol schemes such as file:// alongside directory traversal sequences. Because the application evaluates security boundaries on the un-normalized string and then processes the normalized output, the safety guarantees of the filesystem driver are invalidated. This results in arbitrary file read and write operations depending on the downstream function executing the path.
The core issue is a validation-then-normalization vulnerability within the prefixPath method of src/fs/Local.php. When generating absolute file system paths, the application attempts to verify that the target path does not escape the storage volume root. This validation is delegated to Path::ensurePathIsContained(). Crucially, the application performs this validation check on the raw, uncanonicalized path input before any normalization takes place.
The helper method Path::ensurePathIsContained() calculates directory nesting levels by splitting the path into segments using standard separators (/ and \). It iterates through these segments, incrementing an integer counter for normal folders and decrementing it for parent directory markers (..). If this counter drops below zero, the method concludes that the path has traversed past the root directory and returns false. This simple state machine is effective for relative paths but fails when dealing with unexpected protocol prefixes.
When a path starts with a protocol scheme like file://, the validator splits the prefix into segments, such as file:. The validator treats file: as a regular subdirectory, which increments the internal depth counter. However, when the string is later processed by FileHelper::normalizePath(), a regular expression replaces the protocol prefix file:// with an empty string. The subsequent directory traversal segments (../) are then evaluated during the final canonicalization process, moving the directory depth higher than the root directory.
Because the protocol prefix artificially increases the validation depth, the directory traversal escapes the root directory without triggering the depth counter limit. The containment check succeeds, the protocol prefix is stripped during normalization, and the final absolute path resolved by the PHP engine points to local files outside the storage volume. This represents a classic validation bypass due to incorrect state tracking on desanitized parameters.
To illustrate the vulnerability, we examine the original implementation of the prefixPath method in src/fs/Local.php before the patch:
protected function prefixPath(string $path = ''): string
{
// Step 1: Validate containment on raw path input
if (!Path::ensurePathIsContained($path)) {
throw new FsException("The path `$path` is not contained.");
}
// Step 2: Normalize the path and append to the root directory
return $this->getRootPath() . DIRECTORY_SEPARATOR . FileHelper::normalizePath($path);
}The validator splits the input on forward and backward slashes. Consider the payload a/b/c/d/file://../../../../etc/passwd. The parsed segment list contains ['a', 'b', 'c', 'd', 'file:', '..', '..', '..', '..', 'etc', 'passwd']. The containment check processes these segments, raising the depth counter to five before encountering four parent directory indicators. The final depth counter never falls below zero, and validation succeeds. Next, the string is passed to FileHelper::normalizePath() which executes a regular expression clean-up:
public static function normalizePath($path, $ds = DIRECTORY_SEPARATOR): string
{
// Strip file protocol wrappers
$path = preg_replace('/^(file:\\/\\/)*/i', '', $path);
...
$path = parent::normalizePath($path, $ds);
return $path;
}The regular expression strips out the file:// string, leaving a/b/c/d/../../../../etc/passwd. The underlying parent class resolves the parent directory markers, collapsing the string to ../etc/passwd. The function returns this relative path to prefixPath(), which concatenates it with the root path, producing /var/www/web/uploads/../etc/passwd. This path escapes the restricted boundary.
The vulnerability was corrected by changing the order of operations in src/fs/Local.php to ensure the input is normalized first:
protected function prefixPath(string $path = ''): string
{
// Step 1: Normalize the path first
$path = FileHelper::normalizePath($path);
// Step 2: Perform containment checks on the normalized path
if (!Path::ensurePathIsContained($path)) {
throw new FsException("The path `$path` is not contained.");
}
// Step 3: Use the verified normalized path
return $this->getRootPath() . DIRECTORY_SEPARATOR . $path;
}In the patched version, the input a/b/c/d/file://../../../../etc/passwd is normalized to ../etc/passwd first. When passed to the validator, the initial segment .. immediately reduces the depth counter to -1, resulting in a validation failure and throwing an FsException. This fix is logically sound and completely addresses the desynchronization bug.
Exploitation of this vulnerability requires the attacker to have administrative or specific privileged access within Craft CMS. The attacker must target asset-management endpoints or configurations that accept user-controlled path parameters, such as the asset indexing tool or folders associated with local volumes. Because these tools translate user configurations into physical path resolutions, they act as the execution interface for the underlying filesystem driver.
To construct an exploit payload, the attacker must calculate the necessary depth of the target directory structure. If the volume root is located at /var/www/web/storage/user_uploads, the attacker needs to traverse four directory levels upward to reach the system root. To do this, the attacker creates a payload containing four directory padding segments, a file:// scheme, and four .. traversal sequences.
An example payload is formulated as follows:
folder1/folder2/folder3/folder4/file://../../../../etc/passwdWhen processed by the vulnerable application, the validation function treats folder1 through folder4 and the file: segment as positive depth markers, keeping the overall state positive. After normalization, the padding folders are eliminated by the traversal sequences, leaving only the relative traversal to /etc/passwd. The application subsequently executes the file operation against the system file rather than a subdirectory of the volume root, exposing the target content to the administrative panel.
The concrete security impact of this vulnerability depends on the specific actions performed by the downstream components that call the local filesystem driver. If the path traversal is triggered within a read context, such as downloading or indexing assets, the attacker can read arbitrary files from the local filesystem that are readable by the PHP process. This includes administrative configurations, environmental files, database credentials, and system credentials.
In write contexts, such as renaming or moving files, the vulnerability could allow an attacker to overwrite system files or write files to writable web directories. An attacker could potentially upload a PHP web shell into the public web root by traversing out of the asset storage folder and writing into a publicly accessible directory. This turns the path traversal bypass into an arbitrary code execution vector.
This vulnerability is tracked under GitHub Security Advisory GHSA-7HXC-F267-H5Q7. The vendor has assigned a low-severity rating due to the prerequisite requirement of administrative privileges. However, in environments utilizing multi-tenant setups or delegated administration, this flaw represents an escalation path that compromises the host server. The estimated CVSS score is 4.9, with the vector string CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:L/I:L/A:N reflecting the high privilege requirement.
Remediation requires upgrading Craft CMS core to a secure version. The development team has released patches for the affected branches. Organizations running Craft CMS 4.x must update to version 4.18.2 or later. Organizations running Craft CMS 5.x must update to version 5.10.6 or later. Upgrades are applied through PHP Composer by running the core package update command:
composer update craftcms/cmsTo detect potential exploitation attempts, security operations teams should analyze server request logs and application error logs. Look for instances of administrative actions containing protocol indicators like file:// or file%3A%2F%2F in combination with directory traversal patterns like ../. Additionally, monitor application crash logs for craft\errors\FsException which indicate that a path containment validation failed.
System administrators should implement system-level hardening to limit the impact of directory traversal vulnerabilities. Ensure the web server and PHP-FPM processes run under a low-privileged dedicated user account. This user must be restricted from reading critical system files like /etc/passwd or accessing other directories outside of the active web root. Implementing restricted PHP configurations such as setting open_basedir can also prevent the PHP engine from accessing files outside specified paths.
CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:L/I:L/A:N| Product | Affected Versions | Fixed Version |
|---|---|---|
Craft CMS Craft CMS | >= 4.0.0, < 4.18.2 | 4.18.2 |
Craft CMS Craft CMS | >= 5.0.0, < 5.10.6 | 5.10.6 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-22 |
| Attack Vector | Network |
| CVSS Score | 4.9 |
| Exploit Status | PoC available |
| CISA KEV | Not Listed |
| Ransomware Use | No |
The software uses external input to construct a pathname that should be within a restricted directory, but it does not properly neutralize sequences such as '..' that can resolve to a location outside of the restricted directory.
A critical OS command injection vulnerability exists in PHP_CodeSniffer's VCS blame report modules (Gitblame, Hgblame, Svnblame). Due to inadequate escaping of filenames passed to shell execution wrappers like popen(), an attacker who commits a file with a maliciously crafted name can execute arbitrary commands when the victim generates a blame report.
An authenticated stored Cross-Site Scripting (XSS) vulnerability exists in the Control Panel helper of Craft CMS before version 5.10.8. Due to lack of HTML entity encoding within the elementLabelHtml method, unescaped draft names are rendered directly into administrative interfaces.
An authorization bypass vulnerability in Craft CMS allows unauthenticated or low-privileged users to query and obtain sensitive time-series user registration counts and demographic metrics. This is due to a missing authorization check inside the actionGetNewUsersData endpoint of the ChartsController class.
An authenticated information disclosure vulnerability in Craft CMS allows high-privilege administrators to extract sensitive environment variables, including the CRAFT_SECURITY_KEY and database credentials, using a blind error-based template injection attack within element select condition rules.
A protocol-parsing vulnerability in the pure-Python HTTP/2 library 'h2' (versions <= 4.4.0) allows unauthenticated remote attackers to perform HTTP Request Smuggling (CWE-444). The vulnerability exists because the library does not validate the uniqueness of 'Host' headers in incoming HTTP/2 request streams. When an upstream gateway parses such requests and downgrades them to HTTP/1.1 for internal backend servers, the resulting stream contains duplicate Host headers, which leads to parsing inconsistency and potential bypass of security filters.
An information disclosure vulnerability in Craft CMS allows users with administrative or non-sandboxed template-authoring privileges to read arbitrary system and configuration files. The issue stems from an incomplete class instantiation blocklist in the Twig template extension, which omitted PHP's built-in SplFileObject class.