PST-055: Switching a plugin off does not stop its code: .pth files in the plugins directory run at every process start #784
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#784
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-055. 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/plugins/record.py(98-108)Who can do it, and what they need. A plugin author whose wheel an administrator installed and later switched off (or an administrator who installed something they now distrust). Precondition: the wheel ships a .pth file (uv/pip lay .pth files from a wheel's data dir into --target).
How it works.
evil.pthcontainingimport evil_hook(or an inlineimport os; ...line).importline in every .pth in the directory regardless of plugins.json. Confirmed by scratch test: a .pth beside a record marking the plugin disabled set an environment variable at activate().Impact. A plugin an administrator believes is off keeps arbitrary code running in every process (with the database, SECRET_KEY and the field key in reach). The documented promise that off = 'the code not being there' (policy.py docstring) is false for .pth-carried code. Trust is already extended at install, so this is a hardening gap rather than a new boundary crossing.
Evidence.
Verification (verdict: confirmed; severity after verification: Low). Keep record.py:98-108 as the location. Corrections: (a) the .pth lands in the plugins directory from the wheel's root (purelib), not from a 'data dir'. (b) Neither the page nor the policy.py docstring promises that no code runs; the page says 'its files are still here', so the gap is in the registry docstring and the administrator's expectation, not an explicit false claim. (c) The realistic attackers are a plugin author who deliberately ships a .pth, or a compromised PyPI dependency of a plugin (the .pth lives in the dependency, and switching off the plugin does not stop it). (d) The administrator's only full remedy is Remove, which data.refuse_removing blocks while the plugin holds rows. Suggested fix: do not use site.addsitedir for the plugins directory. Append it to sys.path and process .pth files only for distributions not marked disabled, or drop .pth support. Document that switch-off does not stop import-time code and that Remove or deleting files does.
Suggested remediation. Append the plugins directory to sys.path without processing .pth files (plain sys.path.append, or addsitedir on a per-plugin subdirectory only for enabled plugins), and have read_wheel/check refuse a wheel that contains a .pth (or any file outside its own packages). Say on the Plugins page that 'off' leaves files importable until 'Remove'.
Fixed in
0b8834ffeon main.