Kaynağa Gözat

Refs #32191 -- Added Signer.sign_object()/unsign_object().

Co-authored-by: Craig Smith <hello@craigiansmith.com.au>
Florian Apolloner 4 yıl önce
ebeveyn
işleme
102d92fc09
4 değiştirilmiş dosya ile 126 ekleme ve 32 silme
  1. 40 26
      django/core/signing.py
  2. 9 0
      docs/releases/3.2.txt
  3. 61 6
      docs/topics/signing.txt
  4. 16 0
      tests/signing/tests.py

+ 40 - 26
django/core/signing.py

@@ -107,21 +107,7 @@ def dumps(obj, key=None, salt='django.core.signing', serializer=JSONSerializer,
 
     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 TimestampSigner(key, salt=salt).sign(base64d)
+    return TimestampSigner(key, salt=salt).sign_object(obj, serializer=serializer, compress=compress)
 
 
 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.
     """
-    # TimestampSigner.unsign() returns str but base64 and zlib compression
-    # 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)
+    return TimestampSigner(key, salt=salt).unsign_object(s, serializer=serializer, max_age=max_age)
 
 
 class Signer:
@@ -183,6 +159,44 @@ class Signer:
             return value
         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):
 

+ 9 - 0
docs/releases/3.2.txt

@@ -451,6 +451,15 @@ Security
   ``SECRET_KEY``, and then going on to access ``settings.SECRET_KEY`` will now
   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
 ~~~~~~~~~~~~~
 

+ 61 - 6
docs/topics/signing.txt

@@ -62,6 +62,18 @@ value::
     >>> original
     '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
 ``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.
 
+.. versionchanged:: 3.2
+
+    The ``sign_object()`` and ``unsign_object()`` methods were added.
+
 Using the ``salt`` argument
 ---------------------------
 
@@ -104,11 +120,17 @@ your :setting:`SECRET_KEY`::
     >>> signer = Signer()
     >>> signer.sign('My string')
     'My string:GdMGD6HNQ_qdgxYP8yBZAdAIV1w'
+    >>> signer.sign_object({'message': 'Hello!'})
+    'eyJtZXNzYWdlIjoiSGVsbG8hIn0:Xdc-mOFDjs22KsQAqfVfi8PQSPdo3ckWJxPWwQOFhR4'
     >>> signer = Signer(salt='extra')
     >>> signer.sign('My string')
     'My string:Ee7vGi-ING6n02gkcJ-QLHg6vFw'
     >>> signer.unsign('My string:Ee7vGi-ING6n02gkcJ-QLHg6vFw')
     '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
 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
 secret.
 
+.. versionchanged:: 3.2
+
+    The ``sign_object()`` and ``unsign_object()`` methods were added.
+
 Verifying timestamped values
 ----------------------------
 
@@ -156,23 +182,48 @@ created within a specified period of time::
         otherwise raises ``SignatureExpired``. The ``max_age`` parameter can
         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
 
         The ``algorithm`` parameter was added.
 
+.. _signing-complex-data:
+
 Protecting complex data structures
 ----------------------------------
 
 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
-pickle module, but 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::
+``Signer.sign_object()`` and ``unsign_object()`` methods, or signing module's
+``dumps()`` or ``loads()`` functions (which are shortcuts for
+``TimestampSigner(salt='django.core.signing').sign_object()/unsign_object()``).
+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
-    >>> value = signing.dumps({"foo": "bar"})
+    >>> signer = signing.TimestampSigner()
+    >>> value = signer.sign_object({'foo': 'bar'})
+    >>> value
+    'eyJmb28iOiJiYXIifQ:1kx6R3:D4qGKiptAqo5QW9iv4eNLc6xl4RwiFfes6oOcYhkYnc'
+    >>> signer.unsign_object(value)
+    {'foo': 'bar'}
+    >>> value = signing.dumps({'foo': 'bar'})
     >>> value
-    'eyJmb28iOiJiYXIifQ:1NMg1b:zGcDE4-TCkaeGzLeW9UQwZesciI'
+    'eyJmb28iOiJiYXIifQ:1kx6Rf:LBB39RQmME-SRvilheUe5EmPYRbuDBgQp2tCAi7KGLk'
     >>> signing.loads(value)
     {'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.
     Checks ``max_age`` (in seconds) if given.
+
+.. versionchanged:: 3.2
+
+    The ``sign_object()`` and ``unsign_object()`` methods were added.

+ 16 - 0
tests/signing/tests.py

@@ -122,6 +122,22 @@ class TestSigner(SimpleTestCase):
             with self.assertRaises(signing.BadSignature):
                 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):
         "dumps and loads be reversible for any JSON serializable object"
         objects = [