mirror of https://github.com/django/django.git
Refs #32191 -- Added Signer.sign_object()/unsign_object().
Co-authored-by: Craig Smith <hello@craigiansmith.com.au>
This commit is contained in:
parent
5bcba16c01
commit
102d92fc09
|
@ -107,21 +107,7 @@ def dumps(obj, key=None, salt='django.core.signing', serializer=JSONSerializer,
|
||||||
|
|
||||||
The serializer is expected to return a bytestring.
|
The serializer is expected to return a bytestring.
|
||||||
"""
|
"""
|
||||||
data = serializer().dumps(obj)
|
return TimestampSigner(key, salt=salt).sign_object(obj, serializer=serializer, compress=compress)
|
||||||
|
|
||||||
# Flag for if it's been compressed or not
|
|
||||||
is_compressed = False
|
|
||||||
|
|
||||||
if compress:
|
|
||||||
# Avoid zlib dependency unless compress is being used
|
|
||||||
compressed = zlib.compress(data)
|
|
||||||
if len(compressed) < (len(data) - 1):
|
|
||||||
data = compressed
|
|
||||||
is_compressed = True
|
|
||||||
base64d = b64_encode(data).decode()
|
|
||||||
if is_compressed:
|
|
||||||
base64d = '.' + base64d
|
|
||||||
return TimestampSigner(key, salt=salt).sign(base64d)
|
|
||||||
|
|
||||||
|
|
||||||
def loads(s, key=None, salt='django.core.signing', serializer=JSONSerializer, max_age=None):
|
def loads(s, key=None, salt='django.core.signing', serializer=JSONSerializer, max_age=None):
|
||||||
|
@ -130,17 +116,7 @@ def loads(s, key=None, salt='django.core.signing', serializer=JSONSerializer, ma
|
||||||
|
|
||||||
The serializer is expected to accept a bytestring.
|
The serializer is expected to accept a bytestring.
|
||||||
"""
|
"""
|
||||||
# TimestampSigner.unsign() returns str but base64 and zlib compression
|
return TimestampSigner(key, salt=salt).unsign_object(s, serializer=serializer, max_age=max_age)
|
||||||
# operate on bytes.
|
|
||||||
base64d = TimestampSigner(key, salt=salt).unsign(s, max_age=max_age).encode()
|
|
||||||
decompress = base64d[:1] == b'.'
|
|
||||||
if decompress:
|
|
||||||
# It's compressed; uncompress it first
|
|
||||||
base64d = base64d[1:]
|
|
||||||
data = b64_decode(base64d)
|
|
||||||
if decompress:
|
|
||||||
data = zlib.decompress(data)
|
|
||||||
return serializer().loads(data)
|
|
||||||
|
|
||||||
|
|
||||||
class Signer:
|
class Signer:
|
||||||
|
@ -183,6 +159,44 @@ class Signer:
|
||||||
return value
|
return value
|
||||||
raise BadSignature('Signature "%s" does not match' % sig)
|
raise BadSignature('Signature "%s" does not match' % sig)
|
||||||
|
|
||||||
|
def sign_object(self, obj, serializer=JSONSerializer, compress=False):
|
||||||
|
"""
|
||||||
|
Return URL-safe, hmac signed base64 compressed JSON string.
|
||||||
|
|
||||||
|
If compress is True (not the default), check if compressing using zlib
|
||||||
|
can save some space. Prepend a '.' to signify compression. This is
|
||||||
|
included in the signature, to protect against zip bombs.
|
||||||
|
|
||||||
|
The serializer is expected to return a bytestring.
|
||||||
|
"""
|
||||||
|
data = serializer().dumps(obj)
|
||||||
|
# Flag for if it's been compressed or not.
|
||||||
|
is_compressed = False
|
||||||
|
|
||||||
|
if compress:
|
||||||
|
# Avoid zlib dependency unless compress is being used.
|
||||||
|
compressed = zlib.compress(data)
|
||||||
|
if len(compressed) < (len(data) - 1):
|
||||||
|
data = compressed
|
||||||
|
is_compressed = True
|
||||||
|
base64d = b64_encode(data).decode()
|
||||||
|
if is_compressed:
|
||||||
|
base64d = '.' + base64d
|
||||||
|
return self.sign(base64d)
|
||||||
|
|
||||||
|
def unsign_object(self, signed_obj, serializer=JSONSerializer, **kwargs):
|
||||||
|
# Signer.unsign() returns str but base64 and zlib compression operate
|
||||||
|
# on bytes.
|
||||||
|
base64d = self.unsign(signed_obj, **kwargs).encode()
|
||||||
|
decompress = base64d[:1] == b'.'
|
||||||
|
if decompress:
|
||||||
|
# It's compressed; uncompress it first.
|
||||||
|
base64d = base64d[1:]
|
||||||
|
data = b64_decode(base64d)
|
||||||
|
if decompress:
|
||||||
|
data = zlib.decompress(data)
|
||||||
|
return serializer().loads(data)
|
||||||
|
|
||||||
|
|
||||||
class TimestampSigner(Signer):
|
class TimestampSigner(Signer):
|
||||||
|
|
||||||
|
|
|
@ -451,6 +451,15 @@ Security
|
||||||
``SECRET_KEY``, and then going on to access ``settings.SECRET_KEY`` will now
|
``SECRET_KEY``, and then going on to access ``settings.SECRET_KEY`` will now
|
||||||
raise an :exc:`~django.core.exceptions.ImproperlyConfigured` exception.
|
raise an :exc:`~django.core.exceptions.ImproperlyConfigured` exception.
|
||||||
|
|
||||||
|
* The new ``Signer.sign_object()`` and ``Signer.unsign_object()`` methods allow
|
||||||
|
signing complex data structures. See :ref:`signing-complex-data` for more
|
||||||
|
details.
|
||||||
|
|
||||||
|
Also, :func:`signing.dumps() <django.core.signing.dumps>` and
|
||||||
|
:func:`~django.core.signing.loads` become shortcuts for
|
||||||
|
:meth:`.TimestampSigner.sign_object` and
|
||||||
|
:meth:`~.TimestampSigner.unsign_object`.
|
||||||
|
|
||||||
Serialization
|
Serialization
|
||||||
~~~~~~~~~~~~~
|
~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
|
|
@ -62,6 +62,18 @@ value::
|
||||||
>>> original
|
>>> original
|
||||||
'2.5'
|
'2.5'
|
||||||
|
|
||||||
|
If you wish to protect a list, tuple, or dictionary you can do so using the
|
||||||
|
``sign_object()`` and ``unsign_object()`` methods::
|
||||||
|
|
||||||
|
>>> signed_obj = signer.sign_object({'message': 'Hello!'})
|
||||||
|
>>> signed_obj
|
||||||
|
'eyJtZXNzYWdlIjoiSGVsbG8hIn0:Xdc-mOFDjs22KsQAqfVfi8PQSPdo3ckWJxPWwQOFhR4'
|
||||||
|
>>> obj = signer.unsign_object(signed_obj)
|
||||||
|
>>> obj
|
||||||
|
{'message': 'Hello!'}
|
||||||
|
|
||||||
|
See :ref:`signing-complex-data` for more details.
|
||||||
|
|
||||||
If the signature or value have been altered in any way, a
|
If the signature or value have been altered in any way, a
|
||||||
``django.core.signing.BadSignature`` exception will be raised::
|
``django.core.signing.BadSignature`` exception will be raised::
|
||||||
|
|
||||||
|
@ -93,6 +105,10 @@ generate signatures. You can use a different secret by passing it to the
|
||||||
|
|
||||||
The ``algorithm`` parameter was added.
|
The ``algorithm`` parameter was added.
|
||||||
|
|
||||||
|
.. versionchanged:: 3.2
|
||||||
|
|
||||||
|
The ``sign_object()`` and ``unsign_object()`` methods were added.
|
||||||
|
|
||||||
Using the ``salt`` argument
|
Using the ``salt`` argument
|
||||||
---------------------------
|
---------------------------
|
||||||
|
|
||||||
|
@ -104,11 +120,17 @@ your :setting:`SECRET_KEY`::
|
||||||
>>> signer = Signer()
|
>>> signer = Signer()
|
||||||
>>> signer.sign('My string')
|
>>> signer.sign('My string')
|
||||||
'My string:GdMGD6HNQ_qdgxYP8yBZAdAIV1w'
|
'My string:GdMGD6HNQ_qdgxYP8yBZAdAIV1w'
|
||||||
|
>>> signer.sign_object({'message': 'Hello!'})
|
||||||
|
'eyJtZXNzYWdlIjoiSGVsbG8hIn0:Xdc-mOFDjs22KsQAqfVfi8PQSPdo3ckWJxPWwQOFhR4'
|
||||||
>>> signer = Signer(salt='extra')
|
>>> signer = Signer(salt='extra')
|
||||||
>>> signer.sign('My string')
|
>>> signer.sign('My string')
|
||||||
'My string:Ee7vGi-ING6n02gkcJ-QLHg6vFw'
|
'My string:Ee7vGi-ING6n02gkcJ-QLHg6vFw'
|
||||||
>>> signer.unsign('My string:Ee7vGi-ING6n02gkcJ-QLHg6vFw')
|
>>> signer.unsign('My string:Ee7vGi-ING6n02gkcJ-QLHg6vFw')
|
||||||
'My string'
|
'My string'
|
||||||
|
>>> signer.sign_object({'message': 'Hello!'})
|
||||||
|
'eyJtZXNzYWdlIjoiSGVsbG8hIn0:-UWSLCE-oUAHzhkHviYz3SOZYBjFKllEOyVZNuUtM-I'
|
||||||
|
>>> signer.unsign_object('eyJtZXNzYWdlIjoiSGVsbG8hIn0:-UWSLCE-oUAHzhkHviYz3SOZYBjFKllEOyVZNuUtM-I')
|
||||||
|
{'message': 'Hello!'}
|
||||||
|
|
||||||
Using salt in this way puts the different signatures into different
|
Using salt in this way puts the different signatures into different
|
||||||
namespaces. A signature that comes from one namespace (a particular salt
|
namespaces. A signature that comes from one namespace (a particular salt
|
||||||
|
@ -121,6 +143,10 @@ different salt.
|
||||||
Unlike your :setting:`SECRET_KEY`, your salt argument does not need to stay
|
Unlike your :setting:`SECRET_KEY`, your salt argument does not need to stay
|
||||||
secret.
|
secret.
|
||||||
|
|
||||||
|
.. versionchanged:: 3.2
|
||||||
|
|
||||||
|
The ``sign_object()`` and ``unsign_object()`` methods were added.
|
||||||
|
|
||||||
Verifying timestamped values
|
Verifying timestamped values
|
||||||
----------------------------
|
----------------------------
|
||||||
|
|
||||||
|
@ -156,23 +182,48 @@ created within a specified period of time::
|
||||||
otherwise raises ``SignatureExpired``. The ``max_age`` parameter can
|
otherwise raises ``SignatureExpired``. The ``max_age`` parameter can
|
||||||
accept an integer or a :py:class:`datetime.timedelta` object.
|
accept an integer or a :py:class:`datetime.timedelta` object.
|
||||||
|
|
||||||
|
.. method:: sign_object(obj, serializer=JSONSerializer, compress=False)
|
||||||
|
|
||||||
|
.. versionadded:: 3.2
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
.. versionadded:: 3.2
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
.. versionchanged:: 3.1
|
.. versionchanged:: 3.1
|
||||||
|
|
||||||
The ``algorithm`` parameter was added.
|
The ``algorithm`` parameter was added.
|
||||||
|
|
||||||
|
.. _signing-complex-data:
|
||||||
|
|
||||||
Protecting complex data structures
|
Protecting complex data structures
|
||||||
----------------------------------
|
----------------------------------
|
||||||
|
|
||||||
If you wish to protect a list, tuple or dictionary you can do so using the
|
If you wish to protect a list, tuple or dictionary you can do so using the
|
||||||
signing module's ``dumps`` and ``loads`` functions. These imitate Python's
|
``Signer.sign_object()`` and ``unsign_object()`` methods, or signing module's
|
||||||
pickle module, but use JSON serialization under the hood. JSON ensures that
|
``dumps()`` or ``loads()`` functions (which are shortcuts for
|
||||||
even if your :setting:`SECRET_KEY` is stolen an attacker will not be able
|
``TimestampSigner(salt='django.core.signing').sign_object()/unsign_object()``).
|
||||||
to execute arbitrary commands by exploiting the pickle format::
|
These use JSON serialization under the hood. JSON ensures that even if your
|
||||||
|
:setting:`SECRET_KEY` is stolen an attacker will not be able to execute
|
||||||
|
arbitrary commands by exploiting the pickle format::
|
||||||
|
|
||||||
>>> from django.core import signing
|
>>> from django.core import signing
|
||||||
>>> value = signing.dumps({"foo": "bar"})
|
>>> signer = signing.TimestampSigner()
|
||||||
|
>>> value = signer.sign_object({'foo': 'bar'})
|
||||||
>>> value
|
>>> value
|
||||||
'eyJmb28iOiJiYXIifQ:1NMg1b:zGcDE4-TCkaeGzLeW9UQwZesciI'
|
'eyJmb28iOiJiYXIifQ:1kx6R3:D4qGKiptAqo5QW9iv4eNLc6xl4RwiFfes6oOcYhkYnc'
|
||||||
|
>>> signer.unsign_object(value)
|
||||||
|
{'foo': 'bar'}
|
||||||
|
>>> value = signing.dumps({'foo': 'bar'})
|
||||||
|
>>> value
|
||||||
|
'eyJmb28iOiJiYXIifQ:1kx6Rf:LBB39RQmME-SRvilheUe5EmPYRbuDBgQp2tCAi7KGLk'
|
||||||
>>> signing.loads(value)
|
>>> signing.loads(value)
|
||||||
{'foo': 'bar'}
|
{'foo': 'bar'}
|
||||||
|
|
||||||
|
@ -194,3 +245,7 @@ and tuples) if you pass in a tuple, you will get a list from
|
||||||
|
|
||||||
Reverse of ``dumps()``, raises ``BadSignature`` if signature fails.
|
Reverse of ``dumps()``, raises ``BadSignature`` if signature fails.
|
||||||
Checks ``max_age`` (in seconds) if given.
|
Checks ``max_age`` (in seconds) if given.
|
||||||
|
|
||||||
|
.. versionchanged:: 3.2
|
||||||
|
|
||||||
|
The ``sign_object()`` and ``unsign_object()`` methods were added.
|
||||||
|
|
|
@ -122,6 +122,22 @@ class TestSigner(SimpleTestCase):
|
||||||
with self.assertRaises(signing.BadSignature):
|
with self.assertRaises(signing.BadSignature):
|
||||||
signer.unsign(transform(signed_value))
|
signer.unsign(transform(signed_value))
|
||||||
|
|
||||||
|
def test_sign_unsign_object(self):
|
||||||
|
signer = signing.Signer('predictable-secret')
|
||||||
|
tests = [
|
||||||
|
['a', 'list'],
|
||||||
|
'a string \u2019',
|
||||||
|
{'a': 'dictionary'},
|
||||||
|
]
|
||||||
|
for obj in tests:
|
||||||
|
with self.subTest(obj=obj):
|
||||||
|
signed_obj = signer.sign_object(obj)
|
||||||
|
self.assertNotEqual(obj, signed_obj)
|
||||||
|
self.assertEqual(obj, signer.unsign_object(signed_obj))
|
||||||
|
signed_obj = signer.sign_object(obj, compress=True)
|
||||||
|
self.assertNotEqual(obj, signed_obj)
|
||||||
|
self.assertEqual(obj, signer.unsign_object(signed_obj))
|
||||||
|
|
||||||
def test_dumps_loads(self):
|
def test_dumps_loads(self):
|
||||||
"dumps and loads be reversible for any JSON serializable object"
|
"dumps and loads be reversible for any JSON serializable object"
|
||||||
objects = [
|
objects = [
|
||||||
|
|
Loading…
Reference in New Issue