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.

settings on this page
SESSION_COOKIE_SECURE

Whether the session cookie carries Secure. Off by default, so a site served over HTTPS still ships the session cookie over plaintext unless this is set.

accepts
True | False
default
False
set in
settings.py
Read in: django/conf/global_settings.py @ stable/6.1.x
SESSION_COOKIE_HTTPONLY

Already on. The session cookie is a credential, so blocking script access is correct — and the contrast with CSRF_COOKIE_HTTPONLY below is the point.

accepts
True | False
default
True
set in
settings.py
Read in: django/conf/global_settings.py @ stable/6.1.x
SESSION_COOKIE_SAMESITE

Already Lax, which closes the classic cross-site form post without breaking top-level navigation.

accepts
"Lax" | "Strict" | "None" | False
default
"Lax"
set in
settings.py
Read in: django/conf/global_settings.py @ stable/6.1.x
SESSION_COOKIE_AGE

Session lifetime in seconds. Worth reading beside CSRF_COOKIE_AGE, which is a year — the two cookies expire an order of magnitude apart by design.

accepts
<seconds>
default
1209600 (two weeks)
set in
settings.py
Read in: django/conf/global_settings.py @ stable/6.1.x
CSRF_COOKIE_HTTPONLY

Deliberately off, and hardening guides routinely turn it on as a fix. The CSRF token is not a credential; HttpOnly blocks the AJAX code that legitimately reads it and stops nothing, because anyone running script can read the DOM instead.

accepts
True | False
default
False
set in
settings.py
Read in: django/conf/global_settings.py @ stable/6.1.x
CSRF_COOKIE_SECURE

Same shape as the session equivalent, same default, and set for the same reason.

accepts
True | False
default
False
set in
settings.py
Read in: django/conf/global_settings.py @ stable/6.1.x
CSRF_COOKIE_AGE

A year by default. Setting it to None ties the token to the browser session instead, at the cost of breaking long-lived open forms.

accepts
<seconds> | None
default
31449600 (one year)
set in
settings.py
Read in: django/conf/global_settings.py @ stable/6.1.x
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.