CSRF_TRUSTED_ORIGINS must carry a scheme
Django 4.0 began consulting the Origin header in the CSRF middleware, and changed
the format CSRF_TRUSTED_ORIGINS accepts. Entries that were bare hostnames before
must now include a scheme, and a leading dot must become *..
Most material written before December 2021 shows the old form. Pasting one in produces a config Django refuses at check time, which is the good outcome. The bad one is a value that looks right, passes the check, and still rejects requests once a reverse proxy is in front of the app.
CSRF_TRUSTED_ORIGINS = [ "https://app.example.com", # ".example.com" before 4.0 — the leading dot becomes an explicit wildcard "https://*.example.com",]python manage.py check --deployThe deploy check is the right home for this because it audits the rest of the
deployment surface at the same time — CSRF_COOKIE_SECURE (security.W016) and a
missing CsrfViewMiddleware (security.W003) both surface from the same run. It
belongs in CI rather than in someone’s memory.
Origin checking failed - %s does not match any trusted origins.The request carried an Origin header that matched no entry. Most often the scheme is missing from the setting rather than the origin being genuinely untrusted.
Read in: django/middleware/csrf.py — REASON_BAD_ORIGINAs of Django 4.0, the values in the CSRF_TRUSTED_ORIGINS setting must start with a scheme (usually http:// or https://) but found %s. See the release notes for details.The system check caught the pre-4.0 format before the app served a request. It tests only for the absence of `://`, so it will not catch a stale leading-dot wildcard.
Read in: django/core/checks/compatibility/django_4_0.py — id 4_0.E001Note what the check does not cover. It tests only for the absence of :// in
each value, so a stale .example.com entry that was already migrated to
https://.example.com passes the check and still never matches — the dot form
needed to become *., and nothing warns about that.
If TLS terminates at a proxy and the application speaks plain HTTP behind it,
Django sees http:// and the origin still will not match a https:// entry. That
is a SECURE_PROXY_SSL_HEADER problem wearing a CSRF costume, and it needs the
proxy to genuinely set the header Django is told to trust — otherwise a client can
forge it.
Set both sides together and deploy them together. Changing only the Django side looks like the fix did nothing.
Related
Section titled “Related”- ALLOWED_HOSTS — the neighbouring setting whose wildcard syntax is the exact inverse of this one
- SSL redirect behind a proxy — fixes the
http://mismatch described above - The other half of this failure belongs to the proxy: it has to send the scheme header Django is told to trust, and strip any copy the client supplied.