﻿id	summary	reporter	owner	description	type	status	component	version	severity	resolution	keywords	cc	stage	has_patch	needs_docs	needs_tests	needs_better_patch	easy	ui_ux
37478	Database session backends crash on session keys containing a null character	Markus Magnuson	Markus Magnuson	"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.

{{{#!sh
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.

{{{#!python
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@djangoproject.com, 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."	Bug	assigned	contrib.sessions	dev	Normal		not-security		Accepted	1	0	0	0	0	0
