Opened 38 minutes ago

Last modified 2 minutes ago

#37478 assigned Bug

Database session backends crash on session keys containing a null character

Reported by: Markus Magnuson Owned by: Markus Magnuson
Component: contrib.sessions Version: dev
Severity: Normal Keywords: not-security
Cc: Triage Stage: Accepted
Has patch: yes Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description

A request with the header Cookie: sessionid="AAAAAAAA\000" (printable ASCII only) returns HTTP 500 when using the db or cached_db session backend on PostgreSQL.

http.cookies unquotes the \000 escape into a null character. SessionBase._validate_session_key() accepts the key, because it checks only key and len(key) >= 8. SessionStore._get_session_from_db() (and _aget_session_from_db()) then passes it to the database. PostgreSQL's driver rejects it (psycopg 3 raises DataError, psycopg2 raises ValueError), and the exception propagates out of SessionMiddleware. Because the session loads in middleware, every URL is affected, and the 500 response doesn't clear the cookie, so a client holding it keeps getting 500s until the cookie is removed.

The cache, file and signed_cookies backends, and the db backend on SQLite, return 200 for the same cookie.

To reproduce, save the script below as poc.py, then run these commands in the same directory. They need Docker and Python 3.12 or later; the container is removed on stop.

python3 -m venv djnul-venv
. djnul-venv/bin/activate
docker run --rm -d --name djnul -e POSTGRES_DB=djnul -e POSTGRES_HOST_AUTH_METHOD=trust -p 55432:5432 postgres:17
until docker exec djnul pg_isready -h localhost -U postgres -q; do sleep 1; done
pip install "Django>=6.0,<6.1" "psycopg[binary]"
PGHOST=localhost PGPORT=55432 PGUSER=postgres python poc.py
docker stop djnul

It prints crafted: 500 and normal: 200. To test main instead, install git+https://github.com/django/django.git in place of the Django release. The script sets only the database NAME, so psycopg picks up the host, port and user from the PG* environment variables.

import django
from django.conf import settings

settings.configure(
    DEBUG=False,
    ALLOWED_HOSTS=["testserver"],
    SECRET_KEY="x",
    DATABASES={"default": {"ENGINE": "django.db.backends.postgresql", "NAME": "djnul"}},
    INSTALLED_APPS=["django.contrib.contenttypes", "django.contrib.auth", "django.contrib.sessions"],
    MIDDLEWARE=["django.contrib.sessions.middleware.SessionMiddleware"],
    ROOT_URLCONF="__main__",
)
django.setup()

from django.core.management import call_command
from django.http import HttpResponse
from django.test import Client
from django.urls import path

def view(request):
    request.session.get("x")
    return HttpResponse("ok")

urlpatterns = [path("", view)]

call_command("migrate", run_syncdb=True, verbosity=0)
client = Client(raise_request_exception=False)
print("crafted:", client.get("/", HTTP_COOKIE='sessionid="AAAAAAAA\\000"').status_code)  # 500
print("normal: ", client.get("/", HTTP_COOKIE="sessionid=" + "a" * 32).status_code)  # 200

Tested on Django 6.0.9 and main, with psycopg 3 and psycopg2. The code is the same on 5.2, 6.0, 6.1 and main.

Proposed fix: make _validate_session_key() also reject keys containing a null character, so such a key becomes "no session", the same as an unknown key. Restricting keys to VALID_KEY_CHARS (as the file backend does) would be stricter, but it would break the signed_cookies backend, whose keys are signed strings containing :, -, _ and uppercase letters.

This was first reported to security@…, and the security team asked for a public ticket.

I prepared this report and patch with help from Claude Opus 5.5 (Anthropic) via Claude Code, and I reviewed and verified the output myself.

Change History (4)

comment:1 by Markus Magnuson, 19 minutes ago

Has patch: set

comment:3 by Jacob Walls, 10 minutes ago

Keywords: not-security added
Triage Stage: Unreviewed → Accepted

Thanks!

We asked for a public ticket, as the effect here appears to only amplify security problems in other systems.

in reply to:  3 comment:4 by Markus Magnuson, 2 minutes ago

Replying to Jacob Walls:

Thanks!

We asked for a public ticket, as the effect here appears to only amplify security problems in other systems.

The PR needs a rerun of the initial checks, it currently fails on this ticket not being accepted before I opened the PR.

Note: See TracTickets for help on using tickets.
Back to Top