[Django] #37200: Signer.unsign() doesn't accept max_age, breaking HttpRequest.get_signed_cookie() when SIGNING_BACKEND is not a TimestampSigner

8 views
Skip to first unread message

Django

unread,
Jul 2, 2026, 2:10:58 AM (2 days ago) Jul 2
to django-...@googlegroups.com
#37200: Signer.unsign() doesn't accept max_age, breaking
HttpRequest.get_signed_cookie() when SIGNING_BACKEND is not a
TimestampSigner
-------------------------------------+-------------------------------------
Reporter: Stefan Lilov | Type: Bug
Status: new | Component: HTTP
| handling
Version: 5.2 | Severity: Normal
Keywords: signing, cookies, | Triage Stage:
SIGNING_BACKEND, | Unreviewed
get_signed_cookie |
Has patch: 1 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
-------------------------------------+-------------------------------------
HttpRequest.get_signed_cookie() (via signing._unsign_cookie() on 5.2.15+,
or inline on earlier 5.2.x patches) unconditionally calls .unsign(value,
max_age=max_age) on whatever signer signing.get_cookie_signer() returns —
and that signer's class is controlled entirely by the public, documented
SIGNING_BACKEND setting. But Signer.unsign(self, signed_value) has never
accepted a max_age parameter — only TimestampSigner.unsign(self, value,
max_age=None) does. If a project sets SIGNING_BACKEND to
"django.core.signing.Signer" (a plain, non-expiring signer — a legitimate
use case, e.g. to avoid the timestamp prefix on cookie values), every call
to request.get_signed_cookie() raises TypeError unconditionally,
regardless of whether max_age is actually passed by the caller.

**Minimal reproduction:**


{{{
import django
from django.conf import settings
settings.configure(
SECRET_KEY="x",
SIGNING_BACKEND="django.core.signing.Signer", # valid, documented
override
USE_TZ=True,
)
django.setup()

from django.test import RequestFactory
from django.http import HttpResponse

rf = RequestFactory()
resp = HttpResponse()
resp.set_signed_cookie("k", "v") # signs fine -- Signer.sign() has no
max_age issue

req = rf.get("/")
req.COOKIES["k"] = resp.cookies["k"].value
req.get_signed_cookie("k")
# TypeError: Signer.unsign() got an unexpected keyword argument 'max_age'
}}}


**Expected behavior:** request.get_signed_cookie() works with any
SIGNING_BACKEND that produces a valid signer (mirroring
HttpResponse.set_signed_cookie(), which works fine with a plain Signer
since it only calls .sign()).

**Actual behavior:** TypeError on every call, unconditionally — this isn't
gated on max_age actually being passed a non-None value; the kwarg is
passed either way.
--
Ticket URL: <https://code.djangoproject.com/ticket/37200>
Django <https://code.djangoproject.com/>
The Web framework for perfectionists with deadlines.

Django

unread,
Jul 2, 2026, 2:18:33 AM (2 days ago) Jul 2
to django-...@googlegroups.com
#37200: Signer.unsign() doesn't accept max_age, breaking
HttpRequest.get_signed_cookie() when SIGNING_BACKEND is not a
TimestampSigner
-------------------------------------+-------------------------------------
Reporter: Stefan Lilov | Owner: (none)
Type: Bug | Status: new
Component: HTTP handling | Version: 5.2
Severity: Normal | Resolution:
Keywords: signing, cookies, | Triage Stage:
SIGNING_BACKEND, | Unreviewed
get_signed_cookie |
Has patch: 1 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
-------------------------------------+-------------------------------------
Comment (by Stefan Lilov):

Until this is fixed in core, projects hitting this can work around it
entirely
at the settings level, without touching any call sites, by pointing
`SIGNING_BACKEND` at a thin `Signer` subclass that accepts and ignores
`max_age`:

{{{#!python
# myproject/signing.py
from django.core import signing


class MaxAgeCompatSigner(signing.Signer):
"""
Signer that tolerates the max_age kwarg
HttpRequest.get_signed_cookie()
(via signing._unsign_cookie()) always passes, regardless of
SIGNING_BACKEND. Signer.unsign() doesn't accept max_age at all -- only
TimestampSigner.unsign() does -- so with SIGNING_BACKEND set to a
plain
Signer, request.get_signed_cookie() raises TypeError unconditionally.
This subclass makes SIGNING_BACKEND-selected signers interchangeable
with
TimestampSigner for that purpose, without adopting its timestamp-
prefixed
signing format (i.e. without gaining real expiry enforcement).
"""

def unsign(self, signed_value, max_age=None):
return super().unsign(signed_value)
}}}

{{{#!python
# settings.py
SIGNING_BACKEND = "myproject.signing.MaxAgeCompatSigner"
}}}

This is a strict superset of `Signer`'s existing interface -- `sign()`,
`signature()`, `sign_object()`, and `__init__` are all inherited
unchanged, so
signature output is byte-for-byte identical to what a plain `Signer`
produces
(HMAC computation only depends on `key`/`salt`/`algorithm`/`sep`, none of
which the subclass touches). Existing signed cookies remain valid after
switching -- no forced re-signing or logout. `contrib.messages`'
`CookieStorage` (which also goes through `get_cookie_signer()`) is
unaffected
too, since `unsign_object()` never passes `max_age` in the first place.

The one thing to flag for anyone using this: it's a compatibility shim,
not
real expiry support. `max_age` is silently ignored, exactly as it already
was
whenever `SIGNING_BACKEND` pointed at a plain `Signer` before this ticket
was
filed -- if a project actually needs signature-age enforcement on cookies,
it
should use `TimestampSigner` (the default) rather than this workaround.

Confirmed against 5.2.4, 5.2.14, and 5.2.15 -- the underlying
incompatibility
(`Signer.unsign()` never having a `max_age` param) is identical across all
three, so the workaround applies regardless of patch version.
--
Ticket URL: <https://code.djangoproject.com/ticket/37200#comment:1>

Django

unread,
Jul 2, 2026, 6:30:27 AM (2 days ago) Jul 2
to django-...@googlegroups.com
#37200: Signer.unsign() doesn't accept max_age, breaking
HttpRequest.get_signed_cookie() when SIGNING_BACKEND is not a
TimestampSigner
-------------------------------------+-------------------------------------
Reporter: Stefan Lilov | Owner: (none)
Type: Bug | Status: new
Component: HTTP handling | Version: 5.2
Severity: Normal | Resolution:
Keywords: signing, cookies, | Triage Stage: Accepted
SIGNING_BACKEND, |
get_signed_cookie |
Has patch: 1 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
-------------------------------------+-------------------------------------
Changes (by Vishy):

* stage: Unreviewed => Accepted

Comment:

Sharp find! This is an abstraction leak regression introduced during the
fix for [https://github.com/advisories/GHSA-h7pc-vwp9-298g CVE-2026-6873]
in
[https://github.com/django/django/commit/70d36515b9cc71700105a14b275583070d48b689
70d3651].
--
Ticket URL: <https://code.djangoproject.com/ticket/37200#comment:2>

Django

unread,
Jul 2, 2026, 6:42:32 AM (2 days ago) Jul 2
to django-...@googlegroups.com
#37200: Signer.unsign() doesn't accept max_age, breaking
HttpRequest.get_signed_cookie() when SIGNING_BACKEND is not a
TimestampSigner
-------------------------------------+-------------------------------------
Reporter: Stefan Lilov | Owner: Vishy
Type: Bug | Status: assigned
Component: HTTP handling | Version: 5.2
Severity: Normal | Resolution:
Keywords: signing, cookies, | Triage Stage: Accepted
SIGNING_BACKEND, |
get_signed_cookie |
Has patch: 1 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
-------------------------------------+-------------------------------------
Changes (by Vishy):

* owner: (none) => Vishy
* status: new => assigned

--
Ticket URL: <https://code.djangoproject.com/ticket/37200#comment:3>

Django

unread,
Jul 2, 2026, 8:12:13 AM (2 days ago) Jul 2
to django-...@googlegroups.com
#37200: Signer.unsign() doesn't accept max_age, breaking
HttpRequest.get_signed_cookie() when SIGNING_BACKEND is not a
TimestampSigner
-------------------------------------+-------------------------------------
Reporter: Stefan Lilov | Owner: (none)
Type: Bug | Status: new
Component: HTTP handling | Version: 6.1
Severity: Normal | Resolution:
Keywords: signing, cookies, | Triage Stage:
SIGNING_BACKEND, | Unreviewed
get_signed_cookie |
Has patch: 1 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
-------------------------------------+-------------------------------------
Changes (by David Smith):

* owner: Vishy => (none)
* stage: Accepted => Unreviewed
* status: assigned => new
* version: 5.2 => 6.1

Comment:

> Sharp find! This is an abstraction leak regression introduced during the
fix for ​CVE-2026-6873 in ​70d3651.

I'm afraid this triage assessment is incorrect. The reported issue is also
reproducible in the parent of this commit
09fdc06346b167ff6231a4cb80b85ae67b53c715

I was able to reproduce it on the stable/5.1.x branch so even if accepted
this wouldn't be a release blocker.
--
Ticket URL: <https://code.djangoproject.com/ticket/37200#comment:4>

Django

unread,
Jul 2, 2026, 9:30:40 AM (2 days ago) Jul 2
to django-...@googlegroups.com
#37200: Signer.unsign() doesn't accept max_age, breaking
HttpRequest.get_signed_cookie() when SIGNING_BACKEND is not a
TimestampSigner
-------------------------------------+-------------------------------------
Reporter: Stefan Lilov | Owner: zky
Type: Bug | Status: assigned
Component: HTTP handling | Version: 6.1
Severity: Normal | Resolution:
Keywords: signing, cookies, | Triage Stage:
SIGNING_BACKEND, | Unreviewed
get_signed_cookie |
Has patch: 1 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
-------------------------------------+-------------------------------------
Changes (by zky):

* owner: (none) => zky
* status: new => assigned

--
Ticket URL: <https://code.djangoproject.com/ticket/37200#comment:5>

Django

unread,
Jul 2, 2026, 11:12:25 PM (2 days ago) Jul 2
to django-...@googlegroups.com
#37200: Signer.unsign() doesn't accept max_age, breaking
HttpRequest.get_signed_cookie() when SIGNING_BACKEND is not a
TimestampSigner
-------------------------------------+-------------------------------------
Reporter: Stefan Lilov | Owner: zky
Type: Bug | Status: assigned
Component: HTTP handling | Version: 6.1
Severity: Normal | Resolution:
Keywords: signing, cookies, | Triage Stage:
SIGNING_BACKEND, | Unreviewed
get_signed_cookie |
Has patch: 1 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
-------------------------------------+-------------------------------------
Comment (by Vishy):

It appears that {{{get_signed_cookie()}}} was introduced in
[https://github.com/django/django/commit/f60d42846365b2bf2f1c9bc7a3007c303122a20b
f60d42]. However, this exposes an abstraction leak.
--
Ticket URL: <https://code.djangoproject.com/ticket/37200#comment:6>
Reply all
Reply to author
Forward
0 new messages