skip to content

A security audit asks for Django's CSRF_COOKIE_HTTPONLY and CSRF_USE_SESSIONS set to True; what does each change, and what breaks?

level: seniorimportance: nice to knowfreq 25%

answer

  1. what can page scripts still read?
  2. the token moves into the DOM
  3. a key inside request.session
  4. SessionMiddleware must come first
  5. anonymous visitors gain sessions

basics

~20 s

CSRF_COOKIE_HTTPONLY hides the csrftoken cookie from JavaScript, so AJAX code must read the token from a rendered {% csrf_token %} input. CSRF_USE_SESSIONS drops the cookie and stores the secret in the session, requiring SessionMiddleware before CsrfViewMiddleware.

solid answer

~40 s

Both default to `False`, and Django's own documentation says neither adds much real protection, since script running on your origin can read the token from the page anyway. `CSRF_COOKIE_HTTPONLY = True` keeps the `csrftoken` cookie but marks it `HttpOnly`, which breaks front-end code that reads `document.cookie`; the token must come from the hidden `csrfmiddlewaretoken` input that `{% csrf_token %}` renders. `CSRF_USE_SESSIONS = True` stops sending a CSRF cookie at all and stores the secret in `request.session` under `_csrftoken`, so `django.contrib.sessions` is required and `SessionMiddleware` must come before `CsrfViewMiddleware`, or the middleware raises `ImproperlyConfigured`. It also means every anonymous visitor who renders a form gets a stored session and a session cookie, the token disappears when the session is flushed or expires, and `CSRF_COOKIE_HTTPONLY` becomes irrelevant because there is no CSRF cookie left to flag.

code

python · 12 lines
python
MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "django.contrib.sessions.middleware.SessionMiddleware",  # before CSRF
    "django.middleware.common.CommonMiddleware",
    "django.middleware.csrf.CsrfViewMiddleware",
    "django.contrib.auth.middleware.AuthenticationMiddleware",
    "django.contrib.messages.middleware.MessageMiddleware",
    "django.middleware.clickjacking.XFrameOptionsMiddleware",
]

CSRF_USE_SESSIONS = True  # secret kept in request.session["_csrftoken"]
CSRF_COOKIE_HTTPONLY = True  # no effect now: no CSRF cookie is sent

go deeper

for a junior

Know that both settings default to False and that turning either on means JavaScript must read the CSRF token from the page, not the cookie.

for a middle

Explain where the secret lives under each setting, the SessionMiddleware ordering requirement, and why HttpOnly becomes moot once sessions hold the secret.

for a senior

Predict the operational fallout of session-stored tokens, from anonymous session writes to forms dying at logout, and push back on audit items that trade real cost for little protection.

for a principal

Frame CSRF hardening as a risk budget: accept cheap audit asks, reject costly ones with the docs' reasoning, and spend effort on XSS prevention and origin allowlists instead.

## What the two settings control By default Django keeps the **CSRF secret** in its own cookie, `csrftoken`, that JavaScript may read. Two settings change that, and auditors often ask for both: | Setting | Default | Where the secret lives | How front-end code gets the token | |---|---|---|---| | neither set | `False` / `False` | `csrftoken` cookie, readable by scripts | read `document.cookie` | | `CSRF_COOKIE_HTTPONLY = True` | `False` | `csrftoken` cookie, flagged `HttpOnly` | read the hidden `csrfmiddlewaretoken` input | | `CSRF_USE_SESSIONS = True` | `False` | `request.session["_csrftoken"]`, no CSRF cookie | read the hidden `csrfmiddlewaretoken` input | Both are about **where the secret is stored**, not about how the token is checked: `CsrfViewMiddleware` still requires a matching token in the POST field or the `X-CSRFToken` header on every unsafe request. ## CSRF_COOKIE_HTTPONLY in practice `HttpOnly` stops page scripts from reading the cookie through `document.cookie`. For CSRF that buys little, and the setting's own documentation says so: a script able to read the cookie is already running on your origin, where it could read the token from the DOM or simply send the forged request itself. The practical consequences: - any AJAX helper that reads `csrftoken` from `document.cookie` now gets nothing and every unsafe request fails; - pages that make AJAX calls must render `{% csrf_token %}` somewhere so scripts can read the hidden input; - a pure single-page front end served from elsewhere needs another way to receive the token, such as a JSON endpoint that returns `get_token(request)`. ## CSRF_USE_SESSIONS in practice Here the secret moves into the session store, which brings session behaviour with it: 1. **Sessions become mandatory.** `django.contrib.sessions` must be installed, and `SessionMiddleware` must appear before `CsrfViewMiddleware` in `MIDDLEWARE`. Otherwise `request.session` does not exist when the CSRF middleware runs, and it raises `ImproperlyConfigured` naming this requirement. The settings reference adds that `SessionMiddleware` must also come before any middleware that can raise an exception leading to an error view, because the default error views need the token. 2. **Anonymous visitors get sessions.** Whenever a page calls `get_token()`, for example by rendering a login or contact form, the secret is written into the session, so the session is saved and a session cookie is sent, for visitors who would otherwise have had none. That is extra writes to whichever backend `SESSION_ENGINE` points at. 3. **The token's lifetime follows the session's.** `logout()` flushes the session, taking the secret with it, and an expired session loses it too. A form left open across either event fails: with no secret left, the reason is "CSRF cookie not set." (the same text Django uses for a missing cookie), and if a later page already issued a new secret, the old token is reported as incorrect. 4. **Cookie settings move.** No CSRF cookie is sent, so `CSRF_COOKIE_HTTPONLY`, `CSRF_COOKIE_AGE` and the other `CSRF_COOKIE_*` attributes stop mattering; for the HTTPS `Referer` check, Django compares against `SESSION_COOKIE_DOMAIN` instead of `CSRF_COOKIE_DOMAIN`. ## Why the default is not session-bound Django's CSRF documentation answers this in its FAQ: tying the token to a session is common in other frameworks, but not doing so lets the protection work for anonymous submissions on sites where visitors have no session, and the cookie-based secret is considered safe. Two other mechanisms narrow what a session binding would add: - `django.contrib.auth.login()` calls `rotate_token()`, so a secret planted before login is useless afterwards; - the `Origin` check, and the strict `Referer` check over HTTPS, reject cross-site requests before the token is even compared. ## How to answer the auditor - Accept `CSRF_COOKIE_HTTPONLY` if the front end can read the token from the DOM; it is cheap, and the docs note it is sometimes required by security auditors. - Weigh `CSRF_USE_SESSIONS` by its costs: session writes for anonymous traffic, middleware-order coupling, and tokens that die with the session. - Check what the site actually sends before and after: with `CSRF_USE_SESSIONS` on, responses stop carrying a `csrftoken` `Set-Cookie`, and anonymous pages that render forms start carrying a session cookie. Both are easy to see in a browser's network panel or in a test that inspects `response.cookies`. - Note what is already on by default: the `csrftoken` cookie is sent with `SameSite=Lax` (`CSRF_COOKIE_SAMESITE`), and the `Origin` check runs on every unsafe request that carries the header. - Point the review at settings that change more: `CSRF_COOKIE_SECURE`, a tight `CSRF_TRUSTED_ORIGINS`, and the absence of XSS, since cross-site scripting defeats every CSRF defence.

  • Does CSRF_COOKIE_HTTPONLY do anything once CSRF_USE_SESSIONS is True?
    No. With `CSRF_USE_SESSIONS` on, `CsrfViewMiddleware` writes the secret into the session and sends no `csrftoken` cookie, so there is nothing for `CSRF_COOKIE_HTTPONLY` to flag. The session cookie carrying the session key is governed by `SESSION_COOKIE_HTTPONLY`, which already defaults to `True`.
  • With CSRF_USE_SESSIONS on, why does a form left open in one tab 403 after the user logs out in another?
    `logout()` calls `request.session.flush()`, which deletes the session data, including the `_csrftoken` secret. When the old form is submitted, its token matches nothing: if no page has issued a new secret, the middleware finds none and rejects it with "CSRF cookie not set."; if one has, the old token is reported as incorrect. With the default cookie storage the `csrftoken` cookie survives logout, so the same form would still pass the token check.

saying these in an interview costs you the question

  • CSRF_COOKIE_HTTPONLY stops XSS from abusing the CSRF token
  • CSRF_USE_SESSIONS works regardless of middleware order
  • Django's cookie-based token is insecure because it is not session-bound
  • With CSRF_USE_SESSIONS the frontend keeps reading the csrftoken cookie
  • CSRF_USE_SESSIONS costs nothing for anonymous traffic