PST-018: A wildcard POSTULO_ALLOWED_HOSTS is not refused, and absolute links in mail and API answers come from the Host header #747

Closed
opened 2026-10-10 10:55:56 +00:00 by tiagoagueda · 1 comment
Owner

Found in the security audit of 2026-10-10 (SECURITY-AUDIT-2026-10-10.md), run against main at c30851b from 8 to 10 October 2026, as PST-018. 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.


  • Severity: Low
  • Category: host-header-poisoning (operator-misconfiguration precondition)
  • Location: src/postulo/config/settings/base.py (34)
  • Found by: CSRF, methods and redirects

Who can do it, and what they need. An unauthenticated stranger, on an instance whose operator set POSTULO_ALLOWED_HOSTS=* (or a broad suffix) to get a proxy working; nothing in checks.py, prod.py or manage.py check --deploy refuses it, and the install guidance only says to 'set' it.

How it works.

  1. Attacker POSTs the password-reset form for the victim's address with Host: attacker.example.
  2. allauth's get_reset_password_from_key_url uses request.build_absolute_uri, which trusts the validated Host; with * every host validates.
  3. The victim receives a genuine Postulo reset email whose link points at attacker.example; following it hands the reset key to the attacker.
  4. The same host value feeds review_url/rendering_url in API answers, callback_url registered with OAuth providers, and the scheduled-job links when POSTULO_PUBLIC_URL is unset.

Impact. Account takeover via poisoned password-reset links, but only on a misconfigured instance. On the default and documented configuration (explicit host list, Django's host validation, USE_X_FORWARDED_HOST off, forwarding headers stripped from untrusted peers) this is not reachable, which is why it is low.

Evidence.

base.py:34 `ALLOWED_HOSTS = env.list("POSTULO_ALLOWED_HOSTS", default=["localhost", "127.0.0.1"])`; grep of checks.py and settings for a `"*"` guard returned nothing; notifications/base.py:187-188 `if request is not None: return request.build_absolute_uri(path)`.

Verification (verdict: confirmed; severity after verification: Low). Low is right, conditional on the misconfiguration (wildcard or over-broad host list, with a proxy that passes arbitrary Host through) and on a victim click. Real vector: password-reset email, and sign-up confirmation email for an address the attacker supplies. Drop the claims about scheduled-job links, API review_url/rendering_url and OAuth callback_url as separate vectors. Suggested fix: refuse '*' or a leading-dot catch-all in prod settings or a registered system check, matching the existing refusal of weak keys (#111). When POSTULO_PUBLIC_URL is set, build allauth's absolute URLs from it by overriding get_reset_password_from_key_url and the confirmation URL in AccountAdapter. Also make the install docs say to name the host explicitly.

Suggested remediation. Add a start-up check (beside refuse_a_weak_key) that refuses * in POSTULO_ALLOWED_HOSTS in production, and prefer POSTULO_PUBLIC_URL (already validated at start-up) over the request host when building links that go into email.

Found in the security audit of 2026-10-10 (`SECURITY-AUDIT-2026-10-10.md`), run against `main` at `c30851b` from 8 to 10 October 2026, as **PST-018**. 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. --- - **Severity:** Low - **Category:** host-header-poisoning (operator-misconfiguration precondition) - **Location:** `src/postulo/config/settings/base.py` (34) - **Found by:** CSRF, methods and redirects **Who can do it, and what they need.** An unauthenticated stranger, on an instance whose operator set `POSTULO_ALLOWED_HOSTS=*` (or a broad suffix) to get a proxy working; nothing in checks.py, prod.py or `manage.py check --deploy` refuses it, and the install guidance only says to 'set' it. **How it works.** 1. Attacker POSTs the password-reset form for the victim's address with `Host: attacker.example`. 2. allauth's `get_reset_password_from_key_url` uses `request.build_absolute_uri`, which trusts the validated Host; with `*` every host validates. 3. The victim receives a genuine Postulo reset email whose link points at attacker.example; following it hands the reset key to the attacker. 4. The same host value feeds `review_url`/`rendering_url` in API answers, `callback_url` registered with OAuth providers, and the scheduled-job links when POSTULO_PUBLIC_URL is unset. **Impact.** Account takeover via poisoned password-reset links, but only on a misconfigured instance. On the default and documented configuration (explicit host list, Django's host validation, USE_X_FORWARDED_HOST off, forwarding headers stripped from untrusted peers) this is not reachable, which is why it is low. **Evidence.** ```text base.py:34 `ALLOWED_HOSTS = env.list("POSTULO_ALLOWED_HOSTS", default=["localhost", "127.0.0.1"])`; grep of checks.py and settings for a `"*"` guard returned nothing; notifications/base.py:187-188 `if request is not None: return request.build_absolute_uri(path)`. ``` **Verification** (verdict: confirmed; severity after verification: Low). Low is right, conditional on the misconfiguration (wildcard or over-broad host list, with a proxy that passes arbitrary Host through) and on a victim click. Real vector: password-reset email, and sign-up confirmation email for an address the attacker supplies. Drop the claims about scheduled-job links, API review_url/rendering_url and OAuth callback_url as separate vectors. Suggested fix: refuse '*' or a leading-dot catch-all in prod settings or a registered system check, matching the existing refusal of weak keys (#111). When POSTULO_PUBLIC_URL is set, build allauth's absolute URLs from it by overriding get_reset_password_from_key_url and the confirmation URL in AccountAdapter. Also make the install docs say to name the host explicitly. **Suggested remediation.** Add a start-up check (beside `refuse_a_weak_key`) that refuses `*` in POSTULO_ALLOWED_HOSTS in production, and prefer `POSTULO_PUBLIC_URL` (already validated at start-up) over the request host when building links that go into email.
tiagoagueda added this to the 0.6.0 milestone 2026-10-10 10:57:28 +00:00
Author
Owner

Fixed in 9adbcecf6 on main.

Fixed in 9adbcecf6 on main.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Postulo/postulo#747
No description provided.