PST-082: The backup manifest publishes an unsalted SHA-256 of the secret key, and the field-encryption key is a single unsalted hash #811
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#811
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-082. 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/plugins/secrets.py(30-31,44-53; 44-53; 34-36,63-76)Who can do it, and what they need. Someone who obtains a backup archive (the threat model's own row): a leaked off-site copy, a stolen disk, a mis-shared download. Precondition: the operator did not set POSTULO_FIELD_KEY (the default), so the fingerprint is of Django's SECRET_KEY, and that key is a human-chosen value that passed the …
How it works.
manifest.json->secrets.field_key_sha256.sha256(b"postulo-field-key-fingerprint:" + SECRET_KEY): a fixed public prefix, no salt, no work factor._key()issha256("postulo-connection-secrets:" + material)) to decrypt every connection secret, the mail password, OAuth refresh tokens and the instance mailbox credentials in the same archive.Impact. Turns possession of a backup into a route back into the running instance and into every encrypted secret, whenever the key is guessable. For a 64-byte
token_urlsafekey the hash is uncrackable, so the exposure is bounded by key quality; the threat model acknowledges a weak key would be guessable from it, but the gate it relies on only checks length and character variety.Verification (verdict: confirmed; severity after verification: Informational; first-pass claim: Low). Real but not incremental: the cracking oracle also exists in the DB inside the same archive (session MAC, Fernet tokens), and the threat model documents the fingerprint caveat. Drop the 'forge session cookies' impact (sessions are server-side, unsigned random ids); keep 'forge signed reset/confirmation tokens and decrypt Fernet secrets' for a guessable SECRET_KEY. Cite src/postulo/plugins/secrets.py:44-53 and src/postulo/core/backup.py:462. Only mitigation that matters is key quality (the gate checks length and variety only, as keys.py:88-91 itself admits); a slow KDF for the fingerprint would not close the session/Fernet oracles.
Suggested remediation. Make the mark expensive or keyed: store a random 16-byte salt in the manifest and
scrypt/PBKDF2-HMAC-SHA256(high iteration count) of the material under it, or an HMAC under a key derived with HKDF so equality still verifies at restore. Alternatively drop the fingerprint and answer the restore question by trial-decrypting one stored secret. Document that setting POSTULO_FIELD_KEY to a random value keeps SECRET_KEY out of every backup.Also covered by this issue.
src/postulo/plugins/secrets.py(44-53); Informational). Nothing beyond what the threat model concedes for a weak key; recorded because the fingerprint is of the whole master secret (not only the derived field key) and costs one hash per guess.src/postulo/plugins/secrets.py(34-36,63-76); Informational). A leaked master secret cannot be fully retired; weak keys are cheap to test. No direct attack on a strong key.Fixed in
3f6fa6211on main.