GHSA-GRM4-WM43-9JH5
Vulnerability from github – Published: 2026-08-06 19:49 – Updated: 2026-08-06 19:49Summary
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/debug_log.csv becomes victim-job/debug_log.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:
$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:
$fileItem = $this->jobAttachmentsStorage->get($this->getAttachmentIdentifier($job, $identifier));
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:
$path = Path::canonicalize($location);
if (str_starts_with($path, '..')) {
throw new \OutOfBoundsException(...);
}
return Path::join($this->prefix, $path);
Therefore:
<authorized-job>/../<victim-job>/debug_log.csv
canonicalizes to:
<victim-job>/debug_log.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 inidentifier. - 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 aFilesystemItem. - 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 debug_log.csv, broken-link-checker_log.csv, and search-index_log.csv, which can contain crawled URLs, referring URLs, tags, and error messages.
{
"affected": [
{
"package": {
"ecosystem": "Packagist",
"name": "contao/contao"
},
"ranges": [
{
"events": [
{
"introduced": "5.7.0"
},
{
"fixed": "5.7.7"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "Packagist",
"name": "contao/core-bundle"
},
"ranges": [
{
"events": [
{
"introduced": "5.7.0"
},
{
"fixed": "5.7.7"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-55825"
],
"database_specific": {
"cwe_ids": [
"CWE-22"
],
"github_reviewed": true,
"github_reviewed_at": "2026-08-06T19:49:59Z",
"nvd_published_at": "2026-07-31T20:16:52Z",
"severity": "LOW"
},
"details": "## Summary\n\nAn 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`.\n\nThe 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/debug_log.csv` becomes `victim-job/debug_log.csv`.\n\nThis 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.\n\n\n## Root Cause\n\n`JobsController::downloadJobAttachment()` checks access to the route `jobUuid` before loading the attachment:\n\n```php\n$job = $this-\u003ejobs-\u003egetByUuid($jobUuid);\n\nif (!$job || !$this-\u003ejobs-\u003ehasAccess($job)) {\n throw $this-\u003ecreateNotFoundException();\n}\n\n$attachment = $this-\u003ejobs-\u003egetAttachment($jobUuid, $identifier);\n```\n\n`Jobs::getAttachment()` then resolves a path built from the authorized job UUID and the attacker-controlled identifier:\n\n```php\n$fileItem = $this-\u003ejobAttachmentsStorage-\u003eget($this-\u003egetAttachmentIdentifier($job, $identifier));\n```\n\n```php\nreturn $job-\u003egetUuid().\u0027/\u0027.$identifier;\n```\n\n`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:\n\n```php\n$path = Path::canonicalize($location);\n\nif (str_starts_with($path, \u0027..\u0027)) {\n throw new \\OutOfBoundsException(...);\n}\n\nreturn Path::join($this-\u003eprefix, $path);\n```\n\nTherefore:\n\n```text\n\u003cauthorized-job\u003e/../\u003cvictim-job\u003e/debug_log.csv\n```\n\ncanonicalizes to:\n\n```text\n\u003cvictim-job\u003e/debug_log.csv\n```\n\nwhich remains inside the `job-attachments` filesystem mount and is accepted.\n\n\n## Recommended Fix\n\nTreat the attachment identifier as a filename, not a path:\n\n- Reject `/`, `\\`, NUL, and dot-segment components in `identifier`.\n- Add a route requirement that prevents slashes in `{identifier}` if nested attachment paths are not intended.\n- After resolving, assert the canonical relative path starts with `\u003cauthorized-job-uuid\u003e/` before returning a `FilesystemItem`.\n- Apply the same identifier validation in `Jobs::addAttachment()` so future producers/extensions cannot write outside the owning job directory.\n\n### Impact\nA low-privileged backend user can read another job\u0027s attachment if they know or obtain the target job UUID and attachment filename. Built-in crawler jobs attach CSV logs such as `debug_log.csv`, `broken-link-checker_log.csv`, and `search-index_log.csv`, which can contain crawled URLs, referring URLs, tags, and error messages.",
"id": "GHSA-grm4-wm43-9jh5",
"modified": "2026-08-06T19:49:59Z",
"published": "2026-08-06T19:49:59Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/contao/contao/security/advisories/GHSA-grm4-wm43-9jh5"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-55825"
},
{
"type": "WEB",
"url": "https://contao.org/en/security-advisories/path-traversal-in-the-jobs-module"
},
{
"type": "WEB",
"url": "https://github.com/FriendsOfPHP/security-advisories/blob/master/contao/contao/CVE-2026-55825.yaml"
},
{
"type": "WEB",
"url": "https://github.com/FriendsOfPHP/security-advisories/blob/master/contao/core-bundle/CVE-2026-55825.yaml"
},
{
"type": "PACKAGE",
"url": "https://github.com/contao/contao"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:L/I:N/A:N",
"type": "CVSS_V3"
}
],
"summary": "Contao: Possible path traversal in job download URIs"
}
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.