SEC2-020: Capture notifications build their links from the request's Host rather than from POSTULO_PUBLIC_URL, undoing the rule #747 set #962
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#962
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 on 2026-10-16 in a read-only security review of
postuloat56fc9d531, checked by a second, independent pass.Severity: Low · Category: Configuration · Reference: CWE-644 (Improper Neutralization of HTTP Headers for Scripting Syntax) / CWE-20 (Improper Input Validation); OWASP A05:2021 (Security Misconfiguration)
Location:
src/postulo/notifications/slow.py:62-71(link)src/postulo/api/api.py:580and:595(base=request.build_absolute_uri("/"))src/postulo/notifications/base.py:182-197(absolute_url, the rule)Issue
absolute_urlwas changed for #747 so that the operator'sPOSTULO_PUBLIC_URLwins over whatever the request'sHostheader claims:The errand that delivers the two capture notifications does the opposite.
api.pystoresbase=request.build_absolute_uri("/")in the errand payload, andslow.linkprefers that stored value and only falls back toabsolute_urlwhen it is empty (its docstring still describes that older precedence):So for
capture_receivedandcapture_batchthe link in the mail, the webhook payload and the browser push is the request's Host even on an instance whose operator setPOSTULO_PUBLIC_URLprecisely to stop that. The precondition is the one #747 named:POSTULO_ALLOWED_HOSTSbroad enough that more than one Host validates (prod refuses a bare*unlessPOSTULO_ALLOW_WILDCARD_HOSTS=true,config/settings/prod.py:25-29, but a wildcard suffix or an over-long list is not refused). The browser notice itself is harmless:static/js/app.js:7074andsw.jsrewrite a foreign-origin URL to/before opening it. The mail body (plugins/email/__init__.py:75-81) and the webhookurlfield carry the link as built.Impact
Someone holding a captures-scoped token (the person's own extension, or whoever took its token) can, on a loosely configured instance, make the person's own notification mail and webhook payload point at a host other than the operator's, which is the phishing shape #747 described. It needs the token and the misconfiguration, so it is Low; but it is a regression of a rule the project wrote down and tests for elsewhere.
Fix
Make
linkapply the same precedence asabsolute_url:POSTULO_PUBLIC_URLwhen set, else the payload's base, else the bare path; and rewrite its docstring so it no longer describes the old order.Done when a test beside
tests/test_hardening_747.pydrivesdeliver()with a payload whosebaseis a second allowed host whilePOSTULO_PUBLIC_URLnames the first, and asserts the deliveredNotification.urlstarts withPOSTULO_PUBLIC_URL.Related
#747 (closed) - fixed
absolute_urland the settings check; this call site keeps the old precedence, so it is an incomplete fix rather than a new issue. #247 (closed) - introduced thebasecarried in the errand.