PST-091: Plugins run in-process with the whole ORM and every secret within reach; plugin themes are handed full model instances #820

Closed
opened 2026-10-10 10:56:58 +00:00 by tiagoagueda · 1 comment
Owner

Found in the security audit of 2026-10-10 (SECURITY-AUDIT-2026-10-10.md), run against main at c30851b from 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.


  • Severity: Informational
  • Category: plugin-trust-boundary
  • Location: src/postulo/plugins/api.py (294-306); src/postulo/documents/rendering.py (460-475)
  • Found by: XSS and CSP, administrator surface and plugins
  • Related: PST-054 (#783), PST-055 (#784)

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.

  1. plugins/api.py hands plugins the model classes (Application, Contact, Interview, SyncLink) and OwnedModel; nothing prevents Application.objects.all() or Connection.objects.all(); a sync is called as plugin.sync(connection, connection.full_config) and may touch any row.
  2. Connection.secrets decrypts for whoever calls it, and postulo.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.
  3. …

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.

  • Plugin theme templates are handed full model instances, so a theme can reach what the threat model says it cannot (the referee's contact, the owner's account) (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 …
Found in the security audit of 2026-10-10 (`SECURITY-AUDIT-2026-10-10.md`), run against `main` at `c30851b` from 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. --- - **Severity:** Informational - **Category:** plugin-trust-boundary - **Location:** `src/postulo/plugins/api.py` (294-306); `src/postulo/documents/rendering.py` (460-475) - **Found by:** XSS and CSP, administrator surface and plugins - **Related:** PST-054 (#783), PST-055 (#784) **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.** 1. `plugins/api.py` hands plugins the model classes (Application, Contact, Interview, SyncLink) and `OwnedModel`; nothing prevents `Application.objects.all()` or `Connection.objects.all()`; a sync is called as `plugin.sync(connection, connection.full_config)` and may touch any row. 2. `Connection.secrets` decrypts for whoever calls it, and `postulo.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. 3. … **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.** - **Plugin theme templates are handed full model instances, so a theme can reach what the threat model says it cannot (the referee's contact, the owner's account)** (`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 …
tiagoagueda added this to the 1.0.0 milestone 2026-10-10 10:58:19 +00:00
Author
Owner

Fixed in 33a391b64 on main.

Fixed in 33a391b64 on main.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Postulo/postulo#820
No description provided.