fix: keep import/export job file paths inside their storage dirs #32
Loading…
Reference in a new issue
No description provided.
Delete branch "jtk/export-path-traversal"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #31 (release blocker).
Bug:
Job.export_changeset/import_changesetcastfile_path,file_name,file_size,user_idand progress fields from the form. A forgedjob[file_path]on a failed export stayed on the job, and deleting the job ranFile.rm(Path.join(base, job.file_path)), so../..deleted any file the app could write.cleanup_expired_jobscalledFile.rm(job.file_path)with no base at all.Fix
export_changesetandimport_changesetcast only:filters.user_idis set on the struct, and an import's file fields come from a newfile_infoargument:create_import_job(user, file_info, attrs \\ %{}). The controller no longer merges form params into it.ImportExport.job_file_path/1resolves a job's file and returns:error(with aLogger.warningcarrying the job id) unless it's inside<storage base>/exports/for exports or the system temp dir for imports.delete_job_file/1wraps it. Controllerdelete/2anddownload/2andcleanup_expired_jobs/0all go through it, so expired exports' files now really get removed (before, the relative key was never joined to the base).Tests
file_path/file_name/file_size/user_id/status(context and controller).file_pathis../sentinelor an absolute path leaves the sentinel file intact. So doescleanup_expired_jobs, which still removes a real expired export.mix precommit: 482 passed, 4 skipped.Out of scope (existing behaviour): with the S3 storage backend, export download/delete still look for a local file.
Review from Nye: APPROVE (posted by Upper Wolfjaw, since Forgejo won't let jfro approve its own PR)
Looks good. The export/import changesets cast only
:filters; user_id and file fields are set on the struct. Checked every caller: the exporter's complete_job writes the relativeexports/<file>key that Storage.store_file returns, and importer/controller update_job only set status/total/filters.job_file_path/1is correct:Path.expand(path, base)handles both relative export keys and absolute import paths. Theroot <> "/"prefix check refuses the bareexportsdir, sibling dirs likeexports2/, and../. Cleanup, delete and download all go through it, and the tests cover the forged form path, traversal on delete/cleanup/download, and the happy path.Non-blocking notes: (a) Path.expand doesn't resolve symlinks, which is fine now that the path is server-set. (b) job_file_root assumes the local backend, as the old code did.