The proxy's protocol header is believed whoever sends it #60

Closed
opened 2026-09-06 16:05:54 +00:00 by tiagoagueda · 0 comments
Owner

What happens

config/settings/prod.py sets, unconditionally:

SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")

From then on, any request carrying X-Forwarded-Proto: https is treated as secure —
including one that arrived over plain HTTP straight from the client. Django's own
documentation is blunt about this: set it only if you are certain the proxy strips the
header from what it receives.

Why it matters on a self-hosted instance

The assumption is that a reverse proxy always sits in front. Plenty of Postulo deployments
will not: the Compose file publishes port 8000, and "just expose the port" is a normal
thing for somebody to do on their own network, or briefly through a port forward.

With the header believed, on such an instance:

  • SECURE_SSL_REDIRECT stops redirecting, because the request already claims to be
    secure;
  • session and CSRF cookies are set with Secure on a connection that is not, so the
    browser then refuses to send them back, and sign-in fails in a way that looks like a
    bug;
  • request.is_secure() is a lie anywhere it is used, including in any absolute URL built
    for an email.

Shape

  • Make it opt-in: POSTULO_TRUST_PROXY_HEADER, default off, and only then set
    SECURE_PROXY_SSL_HEADER. An operator who runs a proxy sets one variable; one who does
    not is not quietly told a lie about their own traffic.
  • The Compose file and the deployment page both assume a proxy, so both mention it where
    somebody will read it before they need it.
  • Consider USE_X_FORWARDED_HOST and X-Forwarded-For while in here: the same argument
    applies to anything else taken on trust from in front, and the rate limits of the
    companion issue key on the address they are told.

Classification

Bug, security. Breaking for anyone already behind a proxy if the default flips — so
the release note has to say it plainly and name the variable.

## What happens `config/settings/prod.py` sets, unconditionally: ```python SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") ``` From then on, any request carrying `X-Forwarded-Proto: https` is treated as secure — including one that arrived over plain HTTP straight from the client. Django's own documentation is blunt about this: set it only if you are certain the proxy strips the header from what it receives. ## Why it matters on a self-hosted instance The assumption is that a reverse proxy always sits in front. Plenty of Postulo deployments will not: the Compose file publishes port 8000, and "just expose the port" is a normal thing for somebody to do on their own network, or briefly through a port forward. With the header believed, on such an instance: - `SECURE_SSL_REDIRECT` stops redirecting, because the request already claims to be secure; - session and CSRF cookies are set with `Secure` on a connection that is not, so the browser then refuses to send them back, and sign-in fails in a way that looks like a bug; - `request.is_secure()` is a lie anywhere it is used, including in any absolute URL built for an email. ## Shape - Make it opt-in: `POSTULO_TRUST_PROXY_HEADER`, default **off**, and only then set `SECURE_PROXY_SSL_HEADER`. An operator who runs a proxy sets one variable; one who does not is not quietly told a lie about their own traffic. - The Compose file and the deployment page both assume a proxy, so both mention it where somebody will read it before they need it. - Consider `USE_X_FORWARDED_HOST` and `X-Forwarded-For` while in here: the same argument applies to anything else taken on trust from in front, and the rate limits of the companion issue key on the address they are told. ## Classification Bug, security. **Breaking for anyone already behind a proxy** if the default flips — so the release note has to say it plainly and name the variable.
tiagoagueda added this to the 0.2.0 milestone 2026-09-06 16:05:54 +00:00
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#60
No description provided.