mirror of
https://github.com/django/django.git
synced 2025-08-04 10:59:45 +00:00
Fixed #27775 -- Added support for custom expiry in signed cookies.
This commit is contained in:
parent
ab1b9cc1b3
commit
126f98dadd
6 changed files with 76 additions and 23 deletions
|
@ -10,13 +10,18 @@ class SessionStore(SessionBase):
|
|||
if signature fails.
|
||||
"""
|
||||
try:
|
||||
return signing.loads(
|
||||
payload_value, timestamp = signing.loads(
|
||||
self.session_key,
|
||||
serializer=self.serializer,
|
||||
# This doesn't handle non-default expiry dates, see #19201
|
||||
max_age=self.get_session_cookie_age(),
|
||||
salt="django.contrib.sessions.backends.signed_cookies",
|
||||
return_timestamp=True,
|
||||
)
|
||||
signing.check_signature_expiry(
|
||||
timestamp, max_age=payload_value.get("_session_expiry")
|
||||
)
|
||||
return payload_value
|
||||
|
||||
except Exception:
|
||||
# BadSignature, ValueError, or unpickling exceptions. If any of
|
||||
# these happen, reset the session.
|
||||
|
|
|
@ -115,6 +115,17 @@ def get_cookie_signer(salt="django.core.signing.get_cookie_signer"):
|
|||
)
|
||||
|
||||
|
||||
def check_signature_expiry(timestamp, max_age=None):
|
||||
if max_age is None:
|
||||
return
|
||||
if isinstance(max_age, datetime.timedelta):
|
||||
max_age = max_age.total_seconds()
|
||||
# Check timestamp is not older than max_age.
|
||||
age = time.time() - timestamp
|
||||
if age > max_age:
|
||||
raise SignatureExpired("Signature age %s > %s seconds" % (age, max_age))
|
||||
|
||||
|
||||
class JSONSerializer:
|
||||
"""
|
||||
Simple wrapper around json to be used in signing.dumps and
|
||||
|
@ -159,6 +170,7 @@ def loads(
|
|||
serializer=JSONSerializer,
|
||||
max_age=None,
|
||||
fallback_keys=None,
|
||||
return_timestamp=False,
|
||||
):
|
||||
"""
|
||||
Reverse of dumps(), raise BadSignature if signature fails.
|
||||
|
@ -171,6 +183,7 @@ def loads(
|
|||
s,
|
||||
serializer=serializer,
|
||||
max_age=max_age,
|
||||
return_timestamp=return_timestamp,
|
||||
)
|
||||
|
||||
|
||||
|
@ -259,19 +272,22 @@ class TimestampSigner(Signer):
|
|||
value = "%s%s%s" % (value, self.sep, self.timestamp())
|
||||
return super().sign(value)
|
||||
|
||||
def unsign(self, value, max_age=None):
|
||||
def unsign(self, value, max_age=None, return_timestamp=False):
|
||||
"""
|
||||
Retrieve original value and check it wasn't signed more
|
||||
than max_age seconds ago.
|
||||
"""
|
||||
result = super().unsign(value)
|
||||
value, timestamp = result.rsplit(self.sep, 1)
|
||||
timestamp = b62_decode(timestamp)
|
||||
if max_age is not None:
|
||||
if isinstance(max_age, datetime.timedelta):
|
||||
max_age = max_age.total_seconds()
|
||||
# Check timestamp is not older than max_age
|
||||
age = time.time() - timestamp
|
||||
if age > max_age:
|
||||
raise SignatureExpired("Signature age %s > %s seconds" % (age, max_age))
|
||||
self.payload_timestamp = b62_decode(timestamp)
|
||||
check_signature_expiry(self.payload_timestamp, max_age=max_age)
|
||||
if return_timestamp:
|
||||
return value, self.payload_timestamp
|
||||
return value
|
||||
|
||||
def unsign_object(self, signed_obj, max_age=None, return_timestamp=False, **kwargs):
|
||||
# First verify the outer max_age boundary
|
||||
value = super().unsign_object(signed_obj, max_age=max_age, **kwargs)
|
||||
if return_timestamp:
|
||||
return value, self.payload_timestamp
|
||||
return value
|
||||
|
|
|
@ -217,7 +217,11 @@ Requests and Responses
|
|||
Security
|
||||
~~~~~~~~
|
||||
|
||||
* ...
|
||||
* The new ``return_timestamp`` parameter for
|
||||
:func:`~django.core.signing.loads`,
|
||||
:meth:`~django.core.signing.TimestampSigner.unsign`, and
|
||||
:meth:`~django.core.signing.TimestampSigner.unsign_object` determines whether
|
||||
a tuple containing the unsigned object and the timestamp is returned.
|
||||
|
||||
Serialization
|
||||
~~~~~~~~~~~~~
|
||||
|
|
|
@ -188,23 +188,37 @@ created within a specified period of time:
|
|||
|
||||
Sign ``value`` and append current timestamp to it.
|
||||
|
||||
.. method:: unsign(value, max_age=None)
|
||||
.. method:: unsign(value, max_age=None, return_timestamp=False)
|
||||
|
||||
Checks if ``value`` was signed less than ``max_age`` seconds ago,
|
||||
otherwise raises ``SignatureExpired``. The ``max_age`` parameter can
|
||||
accept an integer or a :py:class:`datetime.timedelta` object.
|
||||
|
||||
If ``return_timestamp`` is ``True``, returns a tuple of the unsigned
|
||||
value and the timestamp, otherwise the unsigned value is returned.
|
||||
|
||||
.. versionchanged:: 6.0
|
||||
|
||||
The ``return_timestamp`` parameter was added.
|
||||
|
||||
.. method:: sign_object(obj, serializer=JSONSerializer, compress=False)
|
||||
|
||||
Encode, optionally compress, append current timestamp, and sign complex
|
||||
data structure (e.g. list, tuple, or dictionary).
|
||||
|
||||
.. method:: unsign_object(signed_obj, serializer=JSONSerializer, max_age=None)
|
||||
.. method:: unsign_object(signed_obj, serializer=JSONSerializer, max_age=None, return_timestamp=False)
|
||||
|
||||
Checks if ``signed_obj`` was signed less than ``max_age`` seconds ago,
|
||||
otherwise raises ``SignatureExpired``. The ``max_age`` parameter can
|
||||
accept an integer or a :py:class:`datetime.timedelta` object.
|
||||
|
||||
If ``return_timestamp`` is ``True``, returns a tuple of the unsigned
|
||||
object and the timestamp, otherwise the unsigned object is returned.
|
||||
|
||||
.. versionchanged:: 6.0
|
||||
|
||||
The ``return_timestamp`` parameter was added.
|
||||
|
||||
.. _signing-complex-data:
|
||||
|
||||
Protecting complex data structures
|
||||
|
@ -249,7 +263,14 @@ and tuples) if you pass in a tuple, you will get a list from
|
|||
Returns URL-safe, signed base64 compressed JSON string. Serialized object
|
||||
is signed using :class:`~TimestampSigner`.
|
||||
|
||||
.. function:: loads(string, key=None, salt='django.core.signing', serializer=JSONSerializer, max_age=None, fallback_keys=None)
|
||||
.. function:: loads(string, key=None, salt='django.core.signing', serializer=JSONSerializer, max_age=None, fallback_keys=None, return_timestamp=False)
|
||||
|
||||
Reverse of ``dumps()``, raises ``BadSignature`` if signature fails.
|
||||
Checks ``max_age`` (in seconds) if given.
|
||||
|
||||
If ``return_timestamp`` is ``True``, returns a tuple of the unsigned object
|
||||
and the timestamp, otherwise the unsigned value is returned.
|
||||
|
||||
.. versionchanged:: 6.0
|
||||
|
||||
The ``return_timestamp`` parameter was added.
|
||||
|
|
|
@ -1245,14 +1245,6 @@ class CookieSessionTests(SessionTestsMixin, SimpleTestCase):
|
|||
async def test_cycle_async(self):
|
||||
pass
|
||||
|
||||
@unittest.expectedFailure
|
||||
def test_actual_expiry(self):
|
||||
# The cookie backend doesn't handle non-default expiry dates, see #19201
|
||||
super().test_actual_expiry()
|
||||
|
||||
async def test_actual_expiry_async(self):
|
||||
pass
|
||||
|
||||
def test_unpickling_exception(self):
|
||||
# signed_cookies backend should handle unpickle exceptions gracefully
|
||||
# by creating a new session
|
||||
|
|
|
@ -233,6 +233,21 @@ class TestTimestampSigner(SimpleTestCase):
|
|||
with self.assertRaises(signing.SignatureExpired):
|
||||
signer.unsign(ts, max_age=10)
|
||||
|
||||
def test_return_timestamp(self):
|
||||
value = "hello"
|
||||
with freeze_time(123456888):
|
||||
signer = signing.TimestampSigner(key="predictable-key")
|
||||
signed_obj = signer.sign_object(value)
|
||||
data_and_time = signer.unsign_object(signed_obj, return_timestamp=True)
|
||||
self.assertEqual(data_and_time, (value, 123456888))
|
||||
|
||||
def test_return_timestamp_dump_loads(self):
|
||||
value = {"foo": "bar", "baz": 42}
|
||||
with freeze_time(123456789):
|
||||
signed_data = signing.dumps(value)
|
||||
data_and_time = signing.loads(signed_data, return_timestamp=True)
|
||||
self.assertEqual(data_and_time, (value, 123456789))
|
||||
|
||||
|
||||
class TestBase62(SimpleTestCase):
|
||||
def test_base62(self):
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue