PST-051: No port or self-address restriction: members can make the server open TCP connections to any port of any public address #780
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#780
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-051. 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/public_addresses.py(58-95);src/postulo/core/mail.py(172-200; 127-132)Who can do it, and what they need. Any authenticated account (capture 30/h, link check or logo 300/h, webhook test 10/h). Precondition: the target is on a publicly routable address, including the instance host's own public IP or an IP-allowlisted upstream service.
How it works.
https://<public-ip>:<port>/as a career link and press Check:check_detailrecordsConnectError: [Errno 111] Connection refusedvsReadTimeoutvsAnswered 200.(links.py:105, 109-113), so each press distinguishes closed, filtered and open ports on any internet host from the instance's egress IP.http://<host's own public IP>:8000/(a Docker-published gunicorn, a metrics exporter, a database admin UI bound on 0.0.0.0 but filtered for external sources by an interface-based firewall rule): the connection originates on the host itself and may bypass the external filter.fetch_page, public-only client) the body is parsed and, where kept-sources are on, the full HTML is stored and shown back, which is a read primitive against any HTTP service that allowlists the server's IP. The guard only asks whether the resolved address is globally routable; it never asks whether the address is this instance's own, nor restricts ports.Impact. The instance becomes a modest port scanner and HTTP read proxy from its own IP for whoever holds an account: reputation/abuse risk for the operator, and a route into services that trust the server's address. Confidentiality impact is environment-dependent; the guard's documented promise (no private addresses) is kept.
Evidence.
Verification (verdict: confirmed; severity after verification: Low). Keep low, framed as 'no port restriction: server can be used to probe open/closed ports on public hosts from the instance's IP (abuse/reputation), bounded by 300/h link-check and 30/h capture limits'. Drop or soften: (a) 'HTTP read proxy' (only text/html responses that parse as a job posting are ever stored or shown; link check returns a status line only); (b) the host's-own-public-IP bypass, which is speculative. Cited lines public_addresses.py:58-95, links.py:105, fetching.py:216-218 are accurate; logos.py:189-192 not independently re-read. Optional hardening: restrict fetches to ports 80/443 (and perhaps common alternates) or make the exception text uniform for connect failures.
Suggested remediation. Restrict public-only fetches to ports 80 and 443 (and perhaps 8080/8443) unless the operator allows others; refuse addresses that belong to the instance itself (resolve ALLOWED_HOSTS / POSTULO_PUBLIC_URL at start-up and add them to the refusal set); collapse low-level transport errors into one neutral sentence in
check_detailand the logo/capture messages instead of the exception text.Also covered by this issue.
src/postulo/core/mail.py(172-200); Informational). Discloses the instance host's internal hostname or private address (container or LAN IP) to a member, which the operator may regard as topology information. The sandbox here reports '[127.0.0.1]'. Public destinations only, so the guard works as designed; this is only what the conversation carries.src/postulo/core/mail.py(127-132); Informational). The server can be used to fingerprint services on any public address and port: a connect oracle (already known) plus the first banner line. Public addresses only, so no internal exposure. Slightly more than the open/closed oracle already recorded, and a way to scan from the instance's IP.Fixed in
fc348bb57on main.