Skip to content

Session and CSRF cookie flags

Severity: highApplies to: Django 4.0+Applies to: Django 5.2 LTSApplies to: Django 6.0Facts last verified 2026-07-26 against Django 6.0 (main)

Django sets two cookies that matter. Read straight from global_settings.py:

Setting Default Should be
SESSION_COOKIE_SECURE False True
CSRF_COOKIE_SECURE False True
SESSION_COOKIE_HTTPONLY True leave it
CSRF_COOKIE_HTTPONLY False leave it — see below
SESSION_COOKIE_SAMESITE "Lax" usually leave it
CSRF_COOKIE_SAMESITE "Lax" usually leave it

Both Secure flags default to False because Django cannot know whether your development server speaks HTTPS. Both SameSite values already default to Lax, so that half needs no action.

the fix
settings.py
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
verify it workedRun this in: http response
Terminal window
curl -sI https://example.com/ | grep -i set-cookie

Check the real response rather than the setting — behind a proxy this is exactly where the two disagree. If Secure is missing here while the setting is True, the problem is the proxy hop, not Django. See SSL redirect behind a proxy.

if you see this
You have 'django.middleware.csrf.CsrfViewMiddleware' in your MIDDLEWARE, but you have not set CSRF_COOKIE_SECURE to True. Using a secure-only CSRF cookie makes it more difficult for network traffic sniffers to steal the CSRF token.

The CSRF cookie may be transmitted over plaintext HTTP. Fires only when the middleware is enabled and CSRF_USE_SESSIONS is off.

Read in: django/core/checks/security/csrf.py — id security.W016
Section titled “Why CSRF_COOKIE_HTTPONLY is False on purpose”

This looks like an oversight and gets “fixed” in a lot of hardening guides. It is deliberate, and setting it to True buys close to nothing while breaking things.

The CSRF cookie is not a credential. It is one half of a matched pair — the defence works because an attacker’s page cannot read the cookie to copy its value into the form, and the same-origin policy already prevents that. HttpOnly blocks JavaScript access, but the JavaScript that legitimately needs the token is your own: any AJAX request has to read csrftoken to set the X-CSRFToken header.

So CSRF_COOKIE_HTTPONLY = True breaks standard AJAX patterns and does not stop an attacker who has script execution — at that point they can simply read the token out of the DOM instead.

The session cookie is a credential, which is why SESSION_COOKIE_HTTPONLY defaults to True and should stay there.

SESSION_COOKIE_AGE is two weeks. CSRF_COOKIE_AGE is one year. Both read from source. If you want the CSRF token to expire with the session, set CSRF_USE_SESSIONS = True, which stores the token in the session instead of a cookie and makes CSRF_COOKIE_* irrelevant.

before you ship this

SESSION_COOKIE_SECURE = True means the cookie is not sent over plain HTTP at all — so a local runserver on http://127.0.0.1:8000 cannot log in. Gate both flags on the same environment switch that drives DEBUG rather than hardcoding them, or local development stops working in a way that looks like a broken login form.

Tightening SameSite from Lax to Strict breaks inbound links from other sites: the cookie is withheld even on a top-level navigation, so a user following a link from an email arrives logged out. Lax is the right default for almost every site.