PST-087: Multicast ranges are classified as public by the destination guard #816
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#816
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-087. 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/core/destinations.py(119-146)Who can do it, and what they need. Any user who can make the server dial an address (capture, link check, logo, webhook). Precondition: none beyond an account. No practical effect today because every outbound protocol here is TCP, which cannot complete a handshake to a multicast destination.
How it works.
http://224.0.0.251/(mDNS group),http://239.255.255.250/(SSDP) orhttp://[ff02::1]/.public_addresses_forresolves the literal,is_publicreturns True because Python'sipaddress.is_globalis derived fromis_private, whose table has no multicast entry (verified:is_public(224.0.0.1)andis_public(ff02::1)are both True in this environment).Impact. No read or write path with TCP. The gap only matters if a future plugin or guard consumer uses UDP (e.g. a future mDNS/SSDP lookup) or if a list of refused ranges is copied from here elsewhere. Recorded because the module says it is 'the one question every guard asks' and the multicast ranges are missing from it.
Verification (verdict: confirmed; severity after verification: Informational). Correct step 3 of the attack path: no TCP SYN is sent to the multicast group; the Linux kernel refuses the connect with ENETUNREACH before building a packet (IPv4 verified in this environment; IPv6 from tcp_v6_connect in kernel source). The only effect today is an error message identical to an unreachable public host. Remediation (described): add an explicit
if address.is_multicast: return False(and arguablyis_reserved/is_link_localare already covered by is_global) at the top ofis_public, and a parametrised case for 224.0.0.251, 239.255.255.250 and ff02::1 in tests/security/test_outbound.py, so a future non-TCP consumer ofapprove()inherits the refusal.Suggested remediation. Add
address.is_multicast(andis_reserved,is_unspecifiedfor belt and braces) to the refusal inis_public, with a parametrised test for 224.0.0.0/4, 239.255.255.250, ff02::1 and ff0e::1.Fixed in
3657c0941on main.