Skip to content

fix(csrf): CSRF_TRUSTED_ORIGINS env var for host-rewriting proxies#2901

Merged
vpetersson merged 4 commits into
masterfrom
fix/csrf-trusted-origins-env-var
May 14, 2026
Merged

fix(csrf): CSRF_TRUSTED_ORIGINS env var for host-rewriting proxies#2901
vpetersson merged 4 commits into
masterfrom
fix/csrf-trusted-origins-env-var

Conversation

@vpetersson
Copy link
Copy Markdown
Contributor

Issues Fixed

Fixes #2900.

Description

IIS ARR with the default preserveHostHeader=false rewrites Host upstream before forwarding to anthias-server, so the browser's Origin: https://signage.example.com and uvicorn's Host: anthias.localdomain are genuinely different hostnames. SameHostOriginCsrfMiddleware's fallback only tolerates scheme drift on the same host, not different hosts, so every unsafe POST (uploads, asset controls, etc.) 403s with Origin checking failed.

The pre-rebrand React+DRF stack didn't hit this because uploads went through DRF's Basic-auth API; the new Django+HTMX upload runs through CsrfViewMiddleware directly.

Fix: expose Django's first-class CSRF_TRUSTED_ORIGINS as a comma-separated env var. Operators behind a host-rewriting reverse proxy list the public origin they actually serve under:

CSRF_TRUSTED_ORIGINS=https://signage.example.com

Default is empty — no behaviour change for plain LAN / Caddy-sidecar deployments where the proxy preserves Host upstream. The same-host fallback continues to cover those.

No new proxy-header trust added; no change to request.get_host(), is_secure(), or build_absolute_uri(). Operator opts in explicitly per-deployment.

Three regression tests pin the behaviour: 403 without the knob, 302 with it, and the allowlist stays exact (one trusted origin doesn't open others).

Checklist

  • I have performed a self-review of my own code.
  • New and existing unit tests pass locally and on CI with my changes.
  • I have done an end-to-end test for Raspberry Pi devices.
  • I have tested my changes for x86 devices.
  • I added a documentation for the changes I have made (when necessary).

🤖 Generated with Claude Code

…2900)

IIS ARR with the default ``preserveHostHeader=false`` rewrites the
upstream ``Host`` before forwarding to anthias-server, so the browser's
``Origin: https://signage.example.com`` and uvicorn's
``Host: anthias.localdomain`` are genuinely different hostnames. The
existing ``SameHostOriginCsrfMiddleware`` fallback only tolerates
scheme drift on the *same* host, not different hosts — uploads (and
every other unsafe POST) 403 with ``Origin checking failed``.

The pre-rebrand React+DRF stack didn't hit this because uploads went
through DRF's Basic-auth API where ``SessionAuthentication``'s CSRF
enforcement didn't apply. The new Django+HTMX upload runs through
``CsrfViewMiddleware`` directly, which is why this regression
surfaced after the migration to Django templates.

Fix: expose Django's first-class ``CSRF_TRUSTED_ORIGINS`` setting as a
comma-separated env var. Operators behind a host-rewriting reverse
proxy list the public origin they actually serve under (e.g.
``CSRF_TRUSTED_ORIGINS=https://signage.example.com``); Django's stock
``_origin_verified`` then accepts requests from that origin. Default
is empty, so the same-host fallback continues to cover plain LAN /
Caddy-sidecar deployments where the proxy preserves Host upstream —
no behaviour change for existing setups.

The earlier "intentionally not set" comment was about the wildcard
limitation (Django only honours subdomain wildcards) and didn't rule
out specific hostnames; updated to reflect what's now supported.

Regression coverage in ``tests/test_csrf.py``:

* ``test_iis_rewrite_host_proxy_without_trusted_origin_rejected``
  pins the 403 that justifies the new knob existing.
* ``test_iis_rewrite_host_proxy_with_trusted_origin_passes`` pins
  the fix — listing the public origin makes the POST succeed.
* ``test_trusted_origin_does_not_open_other_hosts`` pins that the
  allowlist stays exact (``signage.example.com`` doesn't open
  ``attacker.example``).

No new proxy-header trust added; no change to ``request.get_host()``,
``is_secure()``, or ``build_absolute_uri()``. The operator opts in
explicitly per-deployment.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@vpetersson vpetersson requested a review from a team as a code owner May 14, 2026 18:31
@vpetersson vpetersson self-assigned this May 14, 2026
@vpetersson vpetersson requested a review from Copilot May 14, 2026 18:31
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an operator-configurable escape hatch for CSRF Origin validation when running Anthias behind host-rewriting reverse proxies (e.g., IIS ARR with preserveHostHeader=false), by exposing Django’s CSRF_TRUSTED_ORIGINS as a comma-separated environment variable and pinning the behavior with regression tests.

Changes:

  • Add CSRF_TRUSTED_ORIGINS parsing from a new CSRF_TRUSTED_ORIGINS env var in Django settings (default empty).
  • Add regression tests covering: rejection without trusted origin, acceptance with trusted origin, and exact allowlist behavior.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
src/anthias_server/django_project/settings.py Introduces env-driven CSRF_TRUSTED_ORIGINS to support host-rewriting proxy deployments.
tests/test_csrf.py Adds Issue #2900 regression tests validating trusted-origin behavior and non-bypass for other hosts.
Comments suppressed due to low confidence (1)

tests/test_csrf.py:230

  • Same issue here: the settings fixture is typed as pytest.FixtureRequest, which doesn't define CSRF_TRUSTED_ORIGINS. This will fail strict mypy checks when the attribute is assigned; use the correct pytest-django settings wrapper type (or Any) instead.
def test_trusted_origin_does_not_open_other_hosts(
    settings: pytest.FixtureRequest,
) -> None:

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread tests/test_csrf.py
Copilot review: ``settings: pytest.FixtureRequest`` was the wrong
annotation — the pytest-django ``settings`` fixture yields a
``pytest_django.fixtures.SettingsWrapper``, not a ``FixtureRequest``.
The mismatch would surface as ``attr-defined`` errors under strict
mypy when assigning ``settings.CSRF_TRUSTED_ORIGINS``.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

Comment thread tests/test_csrf.py
Comment thread src/anthias_server/django_project/settings.py Outdated
Two Copilot review nits from the previous push:

* ``from __future__ import annotations`` makes the
  ``SettingsWrapper`` import a type-only reference at runtime. Even
  though current Ruff tracks that as a used import, parking it under
  ``TYPE_CHECKING`` is the idiomatic shape and stays robust against
  stricter lints landing later.
* The example origin in the ``CSRF_TRUSTED_ORIGINS`` settings comment
  was wrapped mid-hostname (``https://signage.`` / ``example.com``),
  which is easy to misread or copy wrong. Reflow the paragraph so the
  full URL stays on a single line.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

Operators putting Anthias behind nginx / Apache / IIS / Traefik
hit the same CSRF rejection that motivated #2901 the moment their
proxy rewrites the upstream Host (the default for nginx, Apache
mod_proxy, and IIS ARR). The fix landed as the CSRF_TRUSTED_ORIGINS
env var, but nothing on the website surfaces it — the only docs
were the settings.py comment and the PR description.

Add an "Operations" FAQ entry that:

* names the symptom — POSTs 403 with ``Origin checking failed``;
* lists the Host-preservation directive for the five reverse
  proxies operators actually deploy (nginx, Apache, IIS ARR,
  Caddy, Traefik) — preferred path, since it costs one line of
  proxy config and Anthias's same-host fallback then handles
  scheme drift automatically;
* documents ``CSRF_TRUSTED_ORIGINS=https://signage.example.com``
  as the escape hatch for operators who can't touch the proxy
  config;
* notes that the bundled ``./bin/enable_ssl.sh`` Caddy sidecar
  already does the right thing so the FAQ entry only matters for
  third-party proxy setups.

No code change — website data only.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@sonarqubecloud
Copy link
Copy Markdown

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.

@vpetersson vpetersson merged commit c3e86c6 into master May 14, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG]

2 participants