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 , 19 minutes ago
| Has patch: | set |
|---|
comment:2 by , 18 minutes ago
follow-up: 4 comment:3 by , 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.
comment:4 by , 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.
Patch is here: https://github.com/django/django/pull/22252