GHSA-grm4-wm43-9jh5: Path Traversal
Summary
An authenticated backend user who can access one job can request an attachment identifier containing ../ segments and make the job attachment download endpoint read a file from another job directory inside var/job-attachments.
The controller authorizes only the jobUuid route parameter. The later attachment lookup joins that authorized job UUID with the attacker-controlled identifier, then passes the combined path to the virtual filesystem. VirtualFilesystem::resolve() canonicalizes the whole path and only rejects paths that escape the filesystem mount, so authorized-job/../victim-job/debuglog.csv becomes victim-job/debuglog.csv.
This is a cross-job authorization bypass for known job attachment paths. It is not a practical brute-force against unknown jobs because job directories are UUID v4 values.
Root Cause
JobsController::downloadJobAttachment() checks access to the route jobUuid before loading the attachment:
php $job = $this->jobs->getByUuid($jobUuid);
if (!$job || !$this->jobs->hasAccess($job)) { throw $this->createNotFoundException(); }
$attachment = $this->jobs->getAttachment($jobUuid, $identifier);
Jobs::getAttachment() then resolves a path built from the authorized job UUID and the attacker-controlled identifier:
php $fileItem = $this->jobAttachmentsStorage->get($this->getAttachmentIdentifier($job, $identifier));
php return $job->getUuid().'/'.$identifier;
VirtualFilesystem::resolve() canonicalizes the combined path. It rejects absolute paths and paths that start with .., but it does not preserve the authorized job directory as a boundary:
php $path = Path::canonicalize($location);
if (strstartswith($path, '..')) { throw new \OutOfBoundsException(...); }
return Path::join($this->prefix, $path);
Therefore:
text <authorized-job>/../<victim-job>/debuglog.csv
canonicalizes to:
text <victim-job>/debuglog.csv
which remains inside the job-attachments filesystem mount and is accepted.
Recommended Fix
Treat the attachment identifier as a filename, not a path:
- Reject /, \, NUL, and dot-segment components in identifier. - Add a route requirement that prevents slashes in {identifier} if nested attachment paths are not intended. - After resolving, assert the canonical relative path starts with <authorized-job-uuid>/ before returning a FilesystemItem. - Apply the same identifier validation in Jobs::addAttachment() so future producers/extensions cannot write outside the owning job directory.
Impact A low-privileged backend user can read another job's attachment if they know or obtain the target job UUID and attachment filename. Built-in crawler jobs attach CSV logs such as debuglog.csv, broken-link-checkerlog.csv, and search-indexlog.csv, which can contain crawled URLs, referring URLs, tags, and error messages.
Affected Software
Remediation
Recommended actions to resolve this vulnerability, in priority order.
- Upgrade
Upgrade
composer/contao/core-bundleto a version that resolves this vulnerability.Fixed in 5.7.7 - Upgrade
Upgrade
composer/contao/contaoto a version that resolves this vulnerability.Fixed in 5.7.7 - Configuration
Treat the attachment identifier as a filename (not a path) and reject any identifier containing '/', '\', NUL, or dot-segment components (e.g., '.' / '..') before it is used by Jobs::addAttachment() / Jobs::getAttachment() / the download endpoint.
Jobs attachment download endpoint (JobsController::downloadJobAttachment / Jobs::getAttachment) attachment identifier path traversal handling = reject identifiers containing '/', '\', NUL, and dot-segment components - Configuration
Add a route requirement that prevents slashes in the `{identifier}` parameter when nested attachment paths are not intended.
Routes for job attachment identifier (route requirement for `{identifier}`) route requirement to prevent slashes in `{identifier}` = slashed characters disallowed in `{identifier}` - Configuration
After resolving the combined path, assert that the canonical relative path starts with '<authorized-job-uuid>/' before returning a FilesystemItem. This prevents paths like '<authorized-job>/../<victim-job>/debug_log.csv' from being accepted.
Attachment resolution in Jobs::getAttachment / VirtualFilesystem::resolve canonical relative path boundary check = ensure canonical relative path starts with '<authorized-job-uuid>/' - Configuration
Apply the same identifier validation in `Jobs::addAttachment()` so future producers/extensions cannot write outside the owning job directory.
Jobs attachment identifier validation (Jobs::addAttachment) identifier validation at attachment ingestion = same validation as download path - Compensating control
As an external mitigation, restrict backend-user access so a user who can access one job cannot request attachment filenames/identifiers for other job UUIDs (e.g., enforce job-level authorization at the attachment download endpoint, not only on the `jobUuid` route parameter).