PST-080: Hard-linked media files are silently left out of backups #809
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#809
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 in the security audit of 2026-10-10 (
SECURITY-AUDIT-2026-10-10.md), run againstmainatc30851bfrom 8 to 10 October 2026, as PST-080. Severity: Informational.The text below is the audit's entry, unchanged apart from links to the other issues it names. The finding was produced by automated review and checked by a second automated pass; re-check it before acting on it.
src/postulo/core/backup.py(386-394,476-483)Who can do it, and what they need. No attacker; a correctness edge in the archive writer that affects what a restore can bring back.
How it works.
tarfile.add(..., filter=_plain_files_only)adds the first as a regular file and presents the second as a LNKTYPE member; the filter keeps onlyisfile()/isdir(), so the second is dropped from the archive.verify_backupcannot notice (it checks only the database member), and a restore leaves the row pointing at a file that is not there.Impact. A document missing after a restore, with no warning at backup time. Unlikely with files Django's storage writes itself.
Verification (verdict: confirmed; severity after verification: Informational). Impact confirmed as stated, with one addition: the manifest and the BackupReport both say the media count from
_tree_stats(which counts every name), so the archive silently disagrees with its own manifest. Remediation (described): open the TarFile withdereference=Trueso a hard link is stored as a plain copy (note this also follows symlinks, which the current filter deliberately excludes), or keep the filter but have it turn a LNKTYPE member into a REGTYPE copy of its target, and/or havewrite_backupcount members actually added and raise or warn when that differs from_tree_stats. A regression test withos.linkunder MEDIA_ROOT belongs in tests/test_backup.py.Suggested remediation. In the filter, turn a hard-link member into a regular file (
info.type = tarfile.REGTYPEwith the size and the content of the target) or passdereference=True-style handling forislnk(); count what the filter dropped and record it in the manifest so verification can mention it.Fixed in
672bdbf9con main.