Restore: on Windows, member names with backslashes or a drive letter are written outside the media directory #478
Labels
No labels
accessibility
authentication
breaking change
bug
documentation
enhancement
interface
internationalisation
observability
performance
security
tier
1
tier
2
tier
3
tier/4
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
Postulo/postulo#478
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
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?
Found on 2026-09-29 in a read-only review of postulo at
95348908d, running_safe_member_pathfrom the snapshot on Windows with hand-written member names.Checked again on 2026-10-01 against
mainat0a5cad587: still present._safe_member_pathchecks an archive member's name as a POSIX path, refusing..and empty parts split on/.restore_backupthen turns the result into a native path withPath(*relative.parts). On Windows a single POSIX part may contain backslashes and a drive, which the native path treats as separators and an anchor, so the destination leavesMEDIA_ROOTorPOSTULO_PLUGINS_DIR. The code promises that "a hostile archive writes nothing at all" (core/backup.py:526-527), and Postulo is run on Windows for development (the README describes its Windows fallback renderer).core/backup.py:460_safe_member_path();core/backup.py:546destination = roots[prefix] / Path(*relative.parts)inrestore_backup().MEDIA_ROOTatC:\data\media:media/..\..\evil.txtpasses the check and resolves toC:\evil.txt;media/C:\Windows\evil.txtpasses and becomesC:\Windows\evil.txt, because a drive-qualified part replaces the root;media/a/..\..\..\xpasses and resolves toC:\x.Only
media/../xis refused, which is the one shapetests/test_backup.py:263tests.restorethemselves; a restored archive can already install plugin code that loads at the next start, so this widens what a hostile archive reaches rather than creating a new way in.destination, then refuse unlessdestination.resolve()is underroots[prefix].resolve()(Path.is_relative_to), before anything is written; and also refuse any part containing\or:so a name means the same on every platform.Done when
tests/test_backup.py:test_a_hostile_archive_writes_nothingis parametrised withmedia/..\..\escaped.txt,media/C:\escaped.txtandplugins/a/..\..\..\escaped.txt, and each is refused before the database is loaded, on every platform.