{"id":"GHSA-grm4-wm43-9jh5","summary":"Contao: Possible path traversal in job download URIs","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().'/'.$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, '..')) {\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'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.","aliases":["CVE-2026-55825"],"modified":"2026-08-06T20:00:15.424968922Z","published":"2026-08-06T19:49:59Z","database_specific":{"cwe_ids":["CWE-22"],"severity":"LOW","github_reviewed":true,"github_reviewed_at":"2026-08-06T19:49:59Z","nvd_published_at":"2026-07-31T20:16:52Z"},"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"}],"affected":[{"package":{"name":"contao/contao","ecosystem":"Packagist","purl":"pkg:composer/contao/contao"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"5.7.0"},{"fixed":"5.7.7"}]}],"versions":["5.7.0","5.7.1","5.7.2","5.7.3","5.7.4","5.7.5","5.7.6"],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/08/GHSA-grm4-wm43-9jh5/GHSA-grm4-wm43-9jh5.json"}},{"package":{"name":"contao/core-bundle","ecosystem":"Packagist","purl":"pkg:composer/contao/core-bundle"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"5.7.0"},{"fixed":"5.7.7"}]}],"versions":["5.7.0","5.7.1","5.7.2","5.7.3","5.7.4","5.7.5","5.7.6"],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/08/GHSA-grm4-wm43-9jh5/GHSA-grm4-wm43-9jh5.json"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:L/I:N/A:N"}]}