PST-052: Outbound responses are read with no size cap or total deadline, and name resolution has no timeout #781
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#781
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-052. 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.
Related to #365.
src/postulo/plugins/webhook/__init__.py(249-250);src/postulo/resume/links.py(91-105);src/postulo/plugins/browser/webpush.py(240-241);src/postulo/plugins/public_addresses.py(44-50)Who can do it, and what they need. An authenticated user who creates a webhook connection pointing at a server they control (public address; no operator switch needed). Precondition: an account that may use the webhook notifier (shipped, on by default).
How it works.
url=https://attacker.example/hook.post()callshttp.post(...)onclient(); httpx reads the entire body into memory (client.postis non-streaming) and inflatesContent-Encoding: gzip/deflatewhole (httpx default Accept-Encoding; the lock file also installs brotli, so brotli bombs work too). A 10 MB gzip stream inflating to ~10 GB, or a chunked body with no end, grows the gunicorn worker until it is OOM-killed or until gunicorn's timeout;client()is not built withbounded=Trueand nothing openshttp.until, so a body dribbled a byte every 9 s is never cut off by the 10 s per-wait timeout.plugin.postis called from the scheduler for every delivery (src/postulo/notifications/webhooks.py:157), so the attacker's receiver can do this on every event the account produces, with 8 retries each; the scheduler process is shared by every account on the instance.Impact. Denial of service of the scheduler (reminders, deliveries, copies for every account) or of a web worker, by one authenticated account. No cross-account data exposure. The threat model treats DoS as out of scope for a personal instance, but this is a per-account lever on shared instances and the project already bounded the equivalent public-only paths (#321).
Evidence.
Verification (verdict: confirmed; severity after verification: Low). Keep low (availability only; threat model puts DoS against a personal instance out of scope), though on a multi-account instance the scheduler stall is indefinite and shared, which is the strongest form of it. Fix the premise: brotli is installed (uv.lock), so br bombs also work. Cited lines are correct (webhook/init.py:249-250; http.py:301-307; notifications/webhooks.py:157). Remediation: stream the webhook response (client.stream), read at most a few KB with http.read_body-style limits, advertise Accept-Encoding: identity, and wrap the delivery in http.until(deadline), or build the connection client with bounded=True.
Suggested remediation. In
webhook.post, useclient.stream("POST", ...)and read nothing of the body (only the status and the Location header are used), or read it throughhttp.read_bodywith a small limit; open anhttp.until()deadline around the request and build the client bounded. Consider makingclient()bounded by default and offeringread_bodyto plugins as the only way to take a body.Also covered by this issue.
src/postulo/resume/links.py(91-105); Low). Memory exhaustion or a held gunicorn worker on the instance from one authenticated account (a cross-account availability problem on a shared instance), or a user's own worker OOM when a linked third-party site turns hostile. No confidentiality impact. …src/postulo/plugins/browser/webpush.py(240-241); Low). One member delays or stops the instance-wide scheduler for the duration. The reminder stamp (notified_at) is written before sending, so reminders later in the loop are late. Availability only, but across accounts. …src/postulo/plugins/public_addresses.py(44-50); Informational). Short, bounded worker stalls; limited by the fetch and test rates. Noted for completeness since every other wait on the public-only path is cut to a deadline.Fixed in
a38923f4don main.