A weak POSTULO_SECRET_KEY starts the instance, and it also encrypts every stored credential #111
Labels
No labels
accessibility
authentication
breaking change
bug
documentation
enhancement
interface
internationalisation
observability
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#111
Loading…
Add table
Add a link
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 by
A security audit, from the configuration rather than from a probe. Nothing here is
exploitable by a stranger; everything here is one operator's typo away from mattering a
great deal.
What happens now
prod.pyrefuses to start with no key:It says nothing about a short one.
POSTULO_SECRET_KEY=changemestarts perfectly well.Django notices --
security.W009is exactly this check -- but the container entrypoint runs:W009 is a warning, so it prints one line into a start-up log nobody reads and the
instance serves traffic.
SILENCED_SYSTEM_CHECKSsilences onlyW021, so this is not adeliberate exemption; it is the fail level sitting one step above where the check is.
Why it matters more here than in most Django applications
That key does not only sign sessions and password-reset links.
plugins/secrets.py:So by default the Fernet key protecting every stored connection credential -- somebody's
Telegram bot token, their Paperless password, their Nextcloud login -- is derived from the
same value. A guessable
SECRET_KEYis a guessable encryption key for other people'spasswords to other people's services, and the derivation is a single unsalted SHA-256, so
guessing is cheap.
POSTULO_FIELD_KEYexists and avoids that, and it is optional and easy to miss.What would fix it
Refuse at start-up rather than warn, beside the check that is already there: a minimum
length and a refusal of the obvious placeholders. An operator who has genuinely chosen a
short key can be given a variable to say so; one who pasted
changemeshould be stoppedbefore the first request rather than told in a log line.
--fail-level WARNINGin the entrypoint would also do it and would catch the next suchcheck too -- but it turns every future Django security warning into a start-up failure,
which is a larger decision than this issue should make on its own.
What the audit checked and found sound
Recorded so nobody repeats it.
pip-audit --strictover the full dependency set: nothing.check --deploy: otherwise clean.zizmorat the project's own policy: nothing. No CSRFexemptions anywhere, no raw SQL beyond a
SELECT 1health probe, nofields = "__all__",and no form that writes
owneroris_staff.Classification
Security, bug. Tier 1: a foreseeable operator mistake with a large blast radius and an
unusually cheap fix.