PST-043: Backup archives are only self-consistent: nothing authenticates an archive, and a restore writes the plugins directory #772
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#772
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-043. Severity: Low.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(440-467,543-560,620-738; 533-540,549-560);src/postulo/core/views_backups.py(229-280; 229-281,283-327)Who can do it, and what they need. Someone who can alter a backup archive before an administrator restores it: write access to the off-site/cloud location the operator copies archives to, a compromised workstation the admin uploads from (Server settings -> Backups -> Upload at views_backups.py:229-280), or a MITM on an unencrypted copy. Precondition: an administrator later restores that archive (the documented disaster-recovery path).
How it works.
postulo.*entry point - or any .pth/.py - under plugins/, and edit plugins/.plugins.json to list it.sha256of the database member and write it into manifest.json; repack.plugins sync/registry loads the planted package: arbitrary code in the container, plus a silently inserted administrator account.Impact. An operator cannot tell a genuine archive from a tampered one; a tampered restore yields RCE in the container and full data access. The threat model considers a backup only as something that can be read, not modified.
Evidence.
Verification (verdict: confirmed; severity after verification: Low). Attack path correction: the Server settings -> Backups -> Upload route (views_backups.py:229-280) is an administrator's act, and an administrator is already trusted with the instance (they install plugins), so that route gains nothing beyond existing trust; the real path is the command-line
manage.py restoreof a copy that somebody with write access to the backup store (or a MITM on an unencrypted copy) altered. Impact correction: no entry point orplugins syncis needed; a .pth under plugins/ executes at every process start via site.addsitedir in PluginsConfig.ready() (plugins/apps.py:28), andplugins syncat the entrypoint runs activate() too. Remediation: sign the manifest (and have the manifest carry digests of every member) with an HMAC under POSTULO_FIELD_KEY or a dedicated backup key, refuse a plugins/ member on restore unless its package matches the sha256 the plugins record carries, and haverestoreprint a loud notice when an archive is unsigned; or at minimum state in the threat model and the restore page that an archive is trusted as written and must be stored where nobody else can write.Suggested remediation. Add a MAC to the manifest: HMAC-SHA256 over a canonical manifest (which should list a digest for every member, not only the database) under a key derived from the field key with its own prefix (
postulo-backup-mac:). A restore then reports 'authentic' when the key matches (the case the fingerprint already detects) and refuses, or demands --unverified, otherwise. Optionally support an Ed25519 signing key for archives shipped off-site. Document that restoring an archive is equivalent to running its plugins/ contents.Also covered by this issue.
src/postulo/core/views_backups.py(229-280); Informational). No direct compromise: restore is a deliberate operator action and administrators already install plugins. The gaps are consistency with the page's own rule that sensitive backup actions need a fresh authentication, a worker-OOM on a crafted upload (denial of service is out of scope by the threat …src/postulo/core/views_backups.py(229-281,283-327); Informational). Restore of an archive another administrator uploaded is code execution and account injection with no indication on the page (the threat model trusts backups but says nothing about uploaded ones); the unbounded manifest read is a DoS lever for an administrator (out of scope as DoS, noted as …src/postulo/core/backup.py(533-540,549-560); Informational). CPU/worker time on an administrator action; denial of service is out of scope and the actor is already trusted, so this is an observation only.Fixed in
b80f825c1on main.