Filter concepts by levelShowing all levels.

Django · Section 39

Sessions and Cookies

Level
intermediate
Read
20 min
Concepts
2

SessionMiddleware is what makes request.session exist at all, delegating actual storage to SESSION_ENGINE — the database backend (default) is durable, cache trades durability for speed, cached_db combines both, and signed_cookies stores the session data in the cookie itself, cryptographically signed but explicitly NOT encrypted, so nothing sensitive belongs there. SESSION_COOKIE_AGE sets a fixed lifetime from creation, not an idle timeout, unless paired with SESSION_SAVE_EVERY_REQUEST; session.flush() is the real invalidation call, rotating the session key — the same mechanism login()/logout() use to defend against session fixation. Secure (HTTPS-only), HttpOnly (blocks JavaScript access, mitigating XSS-driven theft), and SameSite (restricts cross-site cookie attachment, a partial CSRF mitigation) each close a genuinely different gap. The CSRF relationship: a session cookie is attached automatically by the browser regardless of which site asked for the request, which is exactly what CSRF exploits — the CSRF token is separate proof the request actually originated from the app's own page, and SameSite alone was never meant to fully replace it.

This section

What is true here

  1. SessionMiddleware makes request.session exist; SESSION_ENGINE (db/cache/cached_db/signed_cookies) decides where the data actually lives.
  2. signed_cookies stores session data in the cookie itself — cryptographically signed, but NOT encrypted, so nothing sensitive belongs there.
  3. SESSION_COOKIE_AGE is a fixed lifetime from creation, not an idle timeout, unless SESSION_SAVE_EVERY_REQUEST is also set.
  4. session.flush() rotates the session key — the actual defense against session fixation, used internally by login()/logout().
  5. Secure/HttpOnly/SameSite each stop a different attack; SameSite is a partial CSRF mitigation, not a substitute for the CSRF token.

What you will be able to do

  • Choose the right SESSION_ENGINE for a project's durability/speed/infrastructure trade-off
  • Configure production-appropriate cookie security flags without breaking legitimate cross-site navigation
  • Explain why login() defends against session fixation, and invalidate a session correctly with flush()
  • Articulate how session cookies and CSRF tokens are separate, complementary mechanisms

Session middleware and storage backends

How request.session gets populated, where the data lives, and expiration vs invalidation.

Session middleware and the storage backends

coreintermediate

SessionMiddleware is what makes request.session exist at all — it reads a session id from a cookie, loads that session's data via SESSION_ENGINE, and saves it back at the end of the request if it changed. The database backend (default) stores session data in the django_session table — durable, but a query per session load. The cache backend stores it in whatever CACHES backend is configured (fast, but ephemeral if that cache is ever cleared) — "cached_db" combines both, writing to the database but reading from cache first. The signed_cookies backend stores the session data itself in the cookie (cryptographically signed, not encrypted) — no server-side storage at all, but capped by the browser's ~4KB cookie limit and, being signed rather than encrypted, still readable (not just tamperable) by the client. SESSION_COOKIE_AGE sets expiration; session.flush() invalidates a session outright.

Think of it as

A session needs somewhere to actually live between requests, and Django deliberately makes that a swappable ENGINE (SESSION_ENGINE) rather than one fixed mechanism, because the right trade-off differs by project: the database backend is the safe default (durable, no extra infrastructure), the cache backend trades durability for speed (fine if losing sessions on a cache flush just means re-login, not data loss), and signed_cookies removes server-side storage entirely by putting the data itself in the cookie — which only works because Django signs it (SECRET_KEY-based, tamper-evident) though it is explicitly NOT encrypted, so anything sensitive stored in a signed-cookie session is still readable by the client, just not forgeable. Expiration and invalidation are two different concerns: SESSION_COOKIE_AGE (and SESSION_EXPIRE_AT_BROWSER_CLOSE) control how long a session naturally lives, while session.flush() is an explicit "kill this session right now" — the operation login()/logout() actually perform under the hood, which is why logging out invalidates a session rather than merely expiring it early.

python
request.session["key"] = value
request.session.get("key")
request.session.flush()

What we're doing: Switch a high-traffic site from the default database backend to cached_db, and explicitly flush a session on a security-sensitive action.

settings.py + views.pypython
# settings.py
SESSION_ENGINE = "django.contrib.sessions.backends.cached_db"

# views.py
def force_password_reset(request):
    request.user.set_password(new_password)
    request.session.flush()   # invalidate this session; user must log in again
    return redirect("login")
2
cached_db still writes through to the database on every save — switching engines is a read-path optimization, not a way to skip durability.
7
flush() is stronger than just clearing session data — it also rotates the session key, the same mechanism login()/logout() use to prevent fixation.

Why this works: A password change is exactly the kind of event that should invalidate any existing session outright rather than let it continue — flush() (not just del request.session["..."]) is the correct call because it also generates a new session key, closing off any pre-existing session id an attacker might have already obtained.

Choosing signed_cookies for a session storing anything sensitive, assuming "signed" means "encrypted"

Wrong

python
# SESSION_ENGINE = "django.contrib.sessions.backends.signed_cookies"
request.session["internal_role"] = "senior_underwriter"   # readable by the client!

Better

python
# SESSION_ENGINE left at the default "db" (or cached_db)
request.session["internal_role"] = "senior_underwriter"   # stored server-side, opaque to the client

What you see: Nothing appears broken functionally — the signature check does prevent a TAMPERED cookie from being accepted — but any sensitive value stored in the session is directly visible to the client (and anyone with access to their browser/network capture) simply by base64-decoding the cookie, since signing proves authenticity, not secrecy.

Why: Django's own documentation is explicit that signed_cookies sessions are signed, not encrypted — the cryptographic signature stops a client from CHANGING the value undetected, but does nothing to stop them from READING it. Storing anything a user shouldn't be able to see (roles, internal flags, other users' data) in a signed-cookie session defeats the purpose even though tampering is still blocked.

Where session data actually lives

db / cache / cached_db

  • +Server-side storage
  • +db: durable, one query per load
  • +cache: fast, ephemeral; cached_db: both

signed_cookies

  • No server storage — data lives in the cookie
  • Signed (tamper-evident), NOT encrypted
  • Capped at ~4KB — never store sensitive data
  • db / cache / cached_db
    • Server-side storage
    • db: durable, one query per load
    • cache: fast, ephemeral; cached_db: both
  • signed_cookies
    • No server storage — data lives in the cookie
    • Signed (tamper-evident), NOT encrypted
    • Capped at ~4KB — never store sensitive data

Session storage backends

Session storage backends
SESSION_ENGINEStores dataTrade-off
db (default)django_session tabledurable, one query per session load unless cached elsewhere
cacheconfigured CACHES backendfast, but data is lost if the cache is cleared/evicted
cached_dbboth — cache-first read, database write-throughcache speed + database durability, needs both configured
signed_cookiesthe cookie itselfno server storage at all, but ~4KB limit and NOT encrypted (signed only)

Together

python
# settings.py
SESSION_ENGINE = "django.contrib.sessions.backends.cached_db"
SESSION_COOKIE_AGE = 1209600  # 2 weeks

Remember: SessionMiddleware makes request.session exist by reading/writing a cookie and delegating to SESSION_ENGINE (db by default; cache trades durability for speed; cached_db combines both; signed_cookies needs no server storage but is signed, NOT encrypted — never store anything sensitive there). SESSION_COOKIE_AGE alone is a fixed lifetime, not an idle timeout, unless paired with SESSION_SAVE_EVERY_REQUEST. session.flush() is the real invalidation call — it also rotates the session key.

See also: cookie security and csrf · login logout and authentication backends · the built in middleware stack

Advertisement

Secure/HttpOnly/SameSite, why login() defends against fixation, and how CSRF relates to the session cookie.

Advertisement