PST-050: POSTULO_CONNECTIONS_ALLOW_PRIVATE is one global switch that also disables address pinning and exposes loopback and link-local to every member #779
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#779
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-050. 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/http.py(106-110,133-152)Who can do it, and what they need. A non-administrator account on an instance whose operator set POSTULO_CONNECTIONS_ALLOW_PRIVATE=true (documented as the way to let a connection reach a Paperless or mail relay on the LAN). Precondition: that switch is on.
How it works.
url=http://169.254.169.254/latest/meta-data/orhttp://127.0.0.1:6379/,http://10.0.0.1/etc.;validate()->check_destinationreturns at once (line 108),_guardreturns at once (line 147), so neither the scheme nor the address is checked and the name is not pinned (a rebinding name is now free to flip between lookups).Answered 200.,The receiver answered 403.,ConnectError: ...or, on a 3xx, the receiver's fullLocationheader (redirect_message, lines 253-260), a per-host, per-port open/closed oracle plus one header of response content.check_connectionis called withallow_private=destinations.private_allowed()(own_mail/init.py:190), and the test echoesf"{type(error).__name__}: {error}"including the remote banner/SMTP reply (core/mail.py:131).Impact. Where the switch is on, every account on the instance can map the operator's private network (hosts, open ports, redirect targets, SMTP banners) through the server, including a cloud metadata endpoint. This is explicitly accepted by the threat model as the operator's decision, but the decision is all-or-nothing: there is no way to allow one destination or one CIDR for one plugin without opening everything for everybody, and enabling it also removes pinning and the http/https scheme check for the connection client.
Evidence.
Verification (verdict: confirmed; severity after verification: Low). Accept only: when an operator enables POSTULO_CONNECTIONS_ALLOW_PRIVATE on an instance with more than one account, every account can map the operator's private network through the Test buttons (open/closed, HTTP status, redirect Location, SMTP banner). Remove the pinning/rebinding and scheme-check claims (no effect when all addresses are allowed). It is an accepted operator-level trade-off under THREAT-MODEL rule 5, so report as a design limitation / documentation gap (state in .env.example and the wiki that the switch applies to every account; offer a per-host or CIDR allowlist). Cited lines http.py:106-110, 133-152 and webhook/init.py:253-260 are accurate.
Suggested remediation. Replace the boolean with an allowlist (hostnames/CIDRs, optionally per plugin) so that allowing a LAN Paperless does not allow 169.254.169.254 and 127.0.0.1 for every user; keep pinning and the scheme check on even when private addresses are allowed; always refuse the metadata and loopback ranges unless named explicitly; and do not echo the Location header to the user when the destination is private.
Fixed in
298248f53on main.http://receiver, so notification text leaves in clear #970