skip to content

Django answers a POST from an SPA on https://app.example.com to https://api.example.com with 'Origin checking failed'; what is checked, and what fixes it?

level: middleimportance: should knowfreq 55%

answer

  1. scheme and host, together
  2. Django 4.0 began reading Origin
  3. a list of full origins
  4. wildcard subdomain entries
  5. the SPA must still send a token

basics

~10 s

Since Django 4.0, CsrfViewMiddleware accepts an unsafe request's Origin only if it equals the site's own scheme and host or matches CSRF_TRUSTED_ORIGINS. Add "https://app.example.com" there; the X-CSRFToken token check still runs afterwards.

solid answer

~50 s

For an unsafe request that carries an `Origin` header, `CsrfViewMiddleware` accepts it when the origin equals the request's own scheme plus `request.get_host()`, appears exactly in `CSRF_TRUSTED_ORIGINS`, or matches a wildcard entry such as `"https://*.example.com"`. `https://app.example.com` is a different origin from the API's, so it fails with "Origin checking failed - https://app.example.com does not match any trusted origins." Add the full origin with its scheme; since Django 4.0 an entry without `://` is a system-check error. A trusted origin does not skip the token check, so the SPA still needs the `csrftoken` cookie and an `X-CSRFToken` header. JavaScript on `app.example.com` cannot read a cookie scoped to `api.example.com`, so either set `CSRF_COOKIE_DOMAIN = ".example.com"` or expose the token from a small JSON endpoint. The browser side, `credentials: "include"` and CORS response headers, is not something Django core provides.

code

python · 6 lines
python
# settings.py on the API host, https://api.example.com
CSRF_TRUSTED_ORIGINS = [
    "https://app.example.com",  # scheme required since Django 4.0
]
CSRF_COOKIE_DOMAIN = ".example.com"  # lets scripts on app.example.com read csrftoken
CSRF_COOKIE_SECURE = True

go deeper

for a junior

Recall that Django rejects POSTs from another origin unless CSRF_TRUSTED_ORIGINS lists it, and that each entry includes the scheme, like https://app.example.com.

for a middle

Walk through the order: Origin check, HTTPS Referer fallback, then cookie and token, and explain why the SPA also needs the cookie domain or a token endpoint.

for a senior

Keep the allowlist to exact origins, recognise the proxy scheme mismatch behind the same error, and separate what Django checks from the CORS and credentials work the browser needs.

for a principal

Decide whether front end and API should share a registrable domain and cookie scope at all, weighing CSRF and cookie exposure against serving both from one origin.

## The order of checks **`CsrfViewMiddleware`** (`django.middleware.csrf`) runs its checks in `process_view()`, and the order explains the error: 1. A view marked `csrf_exempt`, or a request using GET, HEAD, OPTIONS or TRACE, is let through. 2. If the request has an **`Origin`** header, the origin must be verified, or the request is rejected with "Origin checking failed - <origin> does not match any trusted origins." 3. If there is no `Origin` and the request is HTTPS (`request.is_secure()`), the **`Referer`** header is checked strictly instead. Over plain HTTP with no `Origin`, neither header is checked. 4. Then, in every case, the `csrftoken` cookie must be present and the token from the `csrfmiddlewaretoken` field or `X-CSRFToken` header must match it. Browsers send `Origin` on cross-origin `fetch()` POSTs, so the SPA's request takes step 2. The `Origin` check was added in **Django 4.0**; before that, only the HTTPS `Referer` check existed, which is why projects upgrading from 3.2 suddenly met this error. ## How an Origin is matched | Candidate | Example | Matches | |---|---|---| | The site's own origin | built from `https` or `http` per `request.is_secure()`, plus `request.get_host()` | `https://api.example.com` only | | Exact entry | `"https://app.example.com"` | that scheme, host and port exactly | | Wildcard entry | `"https://*.example.com"` | `https://example.com` and any `https://….example.com` | Details that trip people up: - Entries must include the **scheme**. `"app.example.com"` fails the system check `4_0.E001` ("values in the CSRF_TRUSTED_ORIGINS setting must start with a scheme"). - Matching is by string, so a trailing slash (`"https://app.example.com/"`) never matches, and a port is part of the origin: a dev server at `http://localhost:5173` needs exactly that entry. - A wildcard is keyed by scheme: `"https://*.example.com"` does not trust `http://` pages. - `ALLOWED_HOSTS` is a different control. It decides which `Host` values the site answers to; it never makes a foreign origin trusted. ## Fixing the SPA scenario 1. **Trust the SPA's origin.** Add `"https://app.example.com"` to `CSRF_TRUSTED_ORIGINS` on the API. Prefer exact origins to a wildcard. 2. **Let the SPA obtain a token.** The `csrftoken` cookie set by `api.example.com` is host-only by default (`CSRF_COOKIE_DOMAIN = None`), so `document.cookie` on `app.example.com` cannot see it. Either widen it with `CSRF_COOKIE_DOMAIN = ".example.com"`, or add a GET endpoint that returns `get_token(request)` in JSON; that call also makes the middleware set the cookie on the API host. 3. **Send it.** Every unsafe request carries `X-CSRFToken`, exactly as a same-origin page would. 4. **Let the browser send credentials.** A cross-origin `fetch()` needs `credentials: "include"`, and the API must answer with CORS headers that allow the SPA's origin and credentials. Django core ships no CORS middleware, so this comes from your own middleware or a third-party package. In **local development** the same rule bites earlier: an SPA dev server on `http://localhost:5173` posting to `runserver` on `http://localhost:8000` is cross-origin because the port differs. Add `"http://localhost:5173"` to a development-only `CSRF_TRUSTED_ORIGINS`. Over plain HTTP there is no `Referer` fallback to worry about, and because browsers do not separate cookies by port, the SPA's scripts can already read the `csrftoken` cookie that `localhost:8000` set. ## Traps - **Believing the trusted origin replaces the token.** It does not; a POST without `X-CSRFToken` still fails with "CSRF token missing." or "CSRF cookie not set." - **Setting only `CSRF_COOKIE_DOMAIN`.** Before 4.0 a shared cookie domain was enough for cross-subdomain POSTs; the Django 4.0 release notes warn that you may now also need `CSRF_TRUSTED_ORIGINS`, because the `Origin` check comes first. The same notes changed the entry format: the old `".example.com"` became `"https://*.example.com"`. - **Over-broad wildcards.** `"https://*.example.com"` trusts every subdomain, including any that serves user content. Django's docs also note that a subdomain able to set cookies for the parent domain can already get around the cookie-plus-token scheme, so untrusted subdomains are a problem in any case. - **The API's own pages failing after moving behind a TLS-terminating proxy.** The site's own origin is computed from `request.is_secure()`. If Django is not told the original request was HTTPS, it computes `http://api.example.com` while the browser sends `https://api.example.com`. The proper fix is the proxy-trust configuration, not listing your own origin as trusted. - **Referrer policy.** For HTTPS requests without `Origin`, a `Referrer-Policy: no-referrer` or `<meta name="referrer" content="no-referrer">` makes the strict `Referer` check fail with "Referer checking failed - no Referer."

  • Is "https://*.example.com" a safe entry for Django's CSRF_TRUSTED_ORIGINS?
    Only if every host under `example.com`, including the bare domain, is under your control. The wildcard trusts all of them for unsafe requests, so a subdomain that serves user uploads or a forgotten marketing site becomes a trusted origin. Exact entries for the hosts that genuinely post to the API are the safer default.
  • Does listing the SPA's origin in CSRF_TRUSTED_ORIGINS let it skip the X-CSRFToken header?
    No. The origin check only decides whether the request may continue; `CsrfViewMiddleware` then requires the `csrftoken` cookie and a matching token from the POST field or the `X-CSRFToken` header. Without the header the request fails with "CSRF token missing."
  • Why does Django check Referer for some HTTPS requests but not for this SPA's POST?
    The strict `Referer` check runs only when the request is HTTPS and has no `Origin` header. Browsers attach `Origin` to cross-origin `fetch()` POSTs, so the middleware verifies the origin instead. When `Referer` is used, it must be HTTPS and match a `CSRF_TRUSTED_ORIGINS` host, the CSRF cookie domain, or, when no cookie domain is set, the request's own host.

A trusted origin is like being on the guest list at a venue's door: it gets you past the doorman, but the cloakroom still asks for the ticket that matches your coat. Django's Origin check is the doorman; the token check still runs after it.

saying these in an interview costs you the question

  • CSRF_TRUSTED_ORIGINS takes bare hostnames like app.example.com
  • A trusted origin skips the CSRF token check entirely
  • ALLOWED_HOSTS controls which origins may POST to the site
  • Django core adds the CORS headers the SPA needs
  • Setting CSRF_COOKIE_DOMAIN alone makes cross-subdomain POSTs pass