Summary
TogglDataImporter::importData() extracts an uploaded ZIP and reads projects.json with no schema/type validation. Each project object's id field - fully attacker-controlled - is concatenated directly into filesystem paths (projects_users/<id>.json, tasks/<id>.json) with no sanitization:
// app/Service/Import/Importers/TogglDataImporter.php:149-153
if (! file_exists($temporaryDirectory->path('projects_users/'.$project->id.'.json'))) {
throw new ImportException('File "projects_users/'.$project->id.'.json" missing in ZIP');
}
Spatie\TemporaryDirectory\TemporaryDirectory::path() (a third-party dependency) doesn't just concatenate strings - it actively mkdir()s any missing parent directory in the resolved path (vendor/spatie/temporary-directory/src/TemporaryDirectory.php:83-98, permission 0777 before umask). Because $project->id can contain ../ sequences, the resolved path can escape the intended temp sandbox entirely, and the missing-directory auto-creation turns what looks like a read-only file_exists() check into a real filesystem write primitive.
ImportController also returns the raw ImportException message verbatim to the API caller, and a different generic error ("Unknown error") surfaces when the resolved target already exists (PHP's mkdir() raising E_WARNING "File exists"). Together these two distinguishable responses give a reliable existence oracle for arbitrary paths on the host.
Proof of Concept
Both of the following were reproduced live, end-to-end, through the real HTTP API - see the attached script for the exact automated reproduction.
Arbitrary directory creation.** A ZIP with a projects.json entry "id": "../../../../../../var/www/html/PATH_TRAVERSAL_POC_<marker>/probe" was submitted via POST /api/v1/organizations/{org}/import (type=toggl_data_importer). The target directory was confirmed absent beforehand and present immediately afterward - independently verified on
the container filesystem (stat), not just inferred from the response.
Filesystem existence oracle.** Submitting an id that resolves to an already-existing path (../../../../../../var/www/html/composer, the importer always appends .json) produces a distinguishable {"message":"Unknown error"} response instead of the "...missing in ZIP" message used for non-existent targets - reliably answering "does this path exist?" for any attacker-chosen path.
Impact
- Arbitrary,
0755 world-readable directory creation anywhere the application's runtime user can write - usable for minor DoS (inode/directory-entry exhaustion via repeated abuse) or to plant directories at paths other code may assume are plain files.
- A reconnaissance oracle for the existence of arbitrary paths on the host (installation layout, sibling containers' bind mounts, etc.).
- Reachable by any registered user via self-service organization creation
- No elevated privileges or victim interaction required.
Summary
TogglDataImporter::importData()extracts an uploaded ZIP and readsprojects.jsonwith no schema/type validation. Each project object'sidfield - fully attacker-controlled - is concatenated directly into filesystem paths (projects_users/<id>.json,tasks/<id>.json) with no sanitization:Spatie\TemporaryDirectory\TemporaryDirectory::path()(a third-party dependency) doesn't just concatenate strings - it activelymkdir()s any missing parent directory in the resolved path (vendor/spatie/temporary-directory/src/TemporaryDirectory.php:83-98, permission0777before umask). Because$project->idcan contain../sequences, the resolved path can escape the intended temp sandbox entirely, and the missing-directory auto-creation turns what looks like a read-onlyfile_exists()check into a real filesystem write primitive.ImportControlleralso returns the rawImportExceptionmessage verbatim to the API caller, and a different generic error ("Unknown error") surfaces when the resolved target already exists (PHP'smkdir()raisingE_WARNING "File exists"). Together these two distinguishable responses give a reliable existence oracle for arbitrary paths on the host.Proof of Concept
Both of the following were reproduced live, end-to-end, through the real HTTP API - see the attached script for the exact automated reproduction.
Arbitrary directory creation.** A ZIP with a
projects.jsonentry"id": "../../../../../../var/www/html/PATH_TRAVERSAL_POC_<marker>/probe"was submitted viaPOST /api/v1/organizations/{org}/import(type=toggl_data_importer). The target directory was confirmed absent beforehand and present immediately afterward - independently verified onthe container filesystem (
stat), not just inferred from the response.Filesystem existence oracle.** Submitting an
idthat resolves to an already-existing path (../../../../../../var/www/html/composer, the importer always appends.json) produces a distinguishable{"message":"Unknown error"}response instead of the"...missing in ZIP"message used for non-existent targets - reliably answering "does this path exist?" for any attacker-chosen path.Impact
0755world-readable directory creation anywhere the application's runtime user can write - usable for minor DoS (inode/directory-entry exhaustion via repeated abuse) or to plant directories at paths other code may assume are plain files.