SEC2-035: /healthz treats a request as forwarded only on X-Forwarded-For, so behind a loopback proxy every visitor is a "probe" #977
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#977
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: Informational · Category: Data Exposure · Reference: CWE-200 (Exposure of Sensitive Information to an Unauthorized Actor); OWASP A05:2021 (Security Misconfiguration)
Location:
src/postulo/core/middleware.py:64-75(HealthProbeMiddleware._is_the_probe: the marker check at:70, the loopback check at:73)src/postulo/core/views.py:66-83(healthzaddsversionwhen_is_local)src/postulo/core/proxy.py:146-154(POSTULO_PROXY_ADDRis set only whenHTTP_X_FORWARDED_FORis present)Issue
The middleware's comment says the marker is left "whenever a trusted peer sent a forwarding header", but it is written for one header only:
A reverse proxy whose packets arrive from a loopback address (bare metal, or a container with
network_mode: host) is trusted by default. nginx adds noX-Forwarded-Forunless told to, and the wiki's proxy section names onlyX-Forwarded-Proto, so this is the documented configuration. Behind such a proxy every visitor'sGET /healthzarrives from127.0.0.1with noX-Forwarded-For, is answered by the probe path before the host check, and_is_localis true, so the exact version goes to the stranger #800 moved it away from.tests/security/test_health_probe_host.py:81-88covers only theX-Forwarded-Forcase.Impact
Version fingerprinting by anybody who can reach the instance, in a deployment the install pages describe; the host-list bypass itself gains nothing on
/healthz, which builds no links. No personal data.Fix
Mark a trusted peer's request as forwarded whenever any forwarding header arrived, not only
X-Forwarded-For: inTrustedProxyMiddleware, setPOSTULO_PROXY_ADDRwhenany(request.META.get(h) for h in FORWARDING_HEADERS), and keep theREMOTE_ADDRrewrite conditional onX-Forwarded-Foras today.views.healthz._is_localthen reads the same marker ("POSTULO_PROXY_ADDR" not in request.META and ...). The marker has only two readers (middleware.py:70,proxy.py:101), both of which want the broader meaning. Done when: a test intests/security/test_health_probe_host.pysendsREMOTE_ADDR=127.0.0.1withHTTP_X_FORWARDED_PROTO=httpsand noX-Forwarded-For, and asserts a 400 for a host not inALLOWED_HOSTSand noversionin the answer with a right host.Related
#800 (closed) - the version disclosure; this is a gap in its fix. #580 (closed) - the probe middleware. #757 (closed) - the untrusted-forwarder marker.