PST-091: Plugins run in-process with the whole ORM and every secret within reach; plugin themes are handed full model instances #820
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#820
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-091. 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/api.py(294-306);src/postulo/documents/rendering.py(460-475)Who can do it, and what they need. A malicious plugin author whose wheel an administrator installs (or a compromised PyPI dependency named by an honest plugin).
How it works.
plugins/api.pyhands plugins the model classes (Application, Contact, Interview, SyncLink) andOwnedModel; nothing preventsApplication.objects.all()orConnection.objects.all(); a sync is called asplugin.sync(connection, connection.full_config)and may touch any row.Connection.secretsdecrypts for whoever calls it, andpostulo.plugins.secrets._key()derives the Fernet key from settings, so 'secrets are encrypted with a key the plugin never sees' holds only against someone reading the database, not against plugin code.Impact. None beyond what THREAT-MODEL.md already states ('a trust boundary, not a sandbox'); recorded so the synthesis can say the claims about plugins ('owner-scoped', 'key the plugin never sees') are organisational, not technical.
Verification (verdict: confirmed; severity after verification: Informational). Informational, as claimed. Only record that the threat-model sentence 'secrets are encrypted with a key the plugin never sees' should be qualified to 'a key not stored in the database', since plugin code in the process can derive it. Also note that the unhashed-dependency gap is already disclosed in the threat model. Line references are approximately right (api.py:294-295, secrets.py:29-37, installing.py:571-612 and 791-830).
Suggested remediation. Document plainly on the Plugins page and in the threat model that an installed plugin can read every account's rows and every stored secret. Longer term: a locked catalogue with hashes for the whole resolved set (already named as 'the remaining piece'), a scrubbed environment for verify_imports, and an owner-scoped accessor on the plugin surface (e.g. …
Also covered by this issue.
src/postulo/documents/rendering.py(460-475); Informational). None beyond what an installed plugin can already do in Python. The THREAT-MODEL (referees, #696) states 'The renderer hands a theme a read-only value built under those two rules and never the entry or the contact, so a …Fixed in
33a391b64on main.templates/directory is searched before every app-directory template, contrary to whatthemes.pypromises #971