PST-028: Per-address limits are keyed on the peer address, which is the proxy for everyone until the operator names the proxy #757
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#757
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-028. 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/core/throttle.py(186-207);src/postulo/core/proxy.py(43-46)Who can do it, and what they need. Anyone on the internet who can reach the instance through its reverse proxy. Precondition: POSTULO_LOGS_ENDPOINT_ENABLED or POSTULO_METRICS_ENABLED with a token, and a reverse proxy that is not loopback and was not added to POSTULO_TRUSTED_PROXIES (a proxy container on the Compose network, or a proxy on another host - the case .env.example:103-111 describes).
How it works.
Authorization: Bearer wrong. throttle.refused() counts them underpostulo:rate:endpoint-refused:logs:<proxy addr>:<window>.Impact. Loss of log shipping / metrics scraping (and therefore of alerting) for as long as the attacker cares to keep it up; no data exposure. The same shared-address effect applies to allauth's IP-keyed limits (sign-in, signup, password reset) behind an un-named proxy. The module documents the trade-off, but the lockout rule (#472) was added after that reasoning and turns a shared identity into an outsider-triggerable denial.
Evidence.
Verification (verdict: confirmed; severity after verification: Low). Cited lines are right. Corrections to the path and precondition: the eleventh wrong request is already answered 429 (consume() raises at used > 10), and the bar then holds for the rest of the hour window. The precondition is narrower than 'any un-named proxy': the operator's collector must reach Postulo through that same un-named proxy, and for /logs the instance must also run with POSTULO_SSL_REDIRECT=false (otherwise an un-named proxy redirect-loops every page and the operator will have fixed the trust setting); /metrics is exempt from the SSL redirect, so a Prometheus scrape through an un-named proxy works and is the realistic target. Impact: loss of metrics/log shipping for an hour at a time, renewable; no data exposure. The shared-address effect on allauth's IP-keyed sign-in limits is real but already acknowledged in proxy.py's docstring. Remediation: when REMOTE_ADDR is itself an untrusted non-loopback peer and no forwarded address survived, either key the wrong-token bar on something the stranger cannot share with the collector (e.g. …
Suggested remediation. Key the wrong-token counter on (address, SHA-256 prefix of the presented token) or exempt a request that presents the correct token from the bar (check the token first, then bar only wrong presenters); alternatively refuse to enable the bar when the peer is a non-trusted non-loopback address and log a warning naming POSTULO_TRUSTED_PROXIES. Surface the 'your proxy is not trusted' state on the Logs settings page as well as Overview.
Also covered by this issue.
src/postulo/core/proxy.py(43-46); Informational). Denial of service only, which the threat model places out of scope; recorded because the limits that are meant to stop guessing are the ones that become a lockout lever, and the fix is one configuration line that the Overview page already shows.Fixed in
ae41694edon main./healthztreats a request as forwarded only onX-Forwarded-For, so behind a loopback proxy every visitor is a "probe" #977