Ticket #37326: 0001-Improved-docs-for-Signer-to-encourage-more-secure-pa.patch

File 0001-Improved-docs-for-Signer-to-encourage-more-secure-pa.patch, 9.0 KB (added by Tim Schilling, 2 hours ago)
  • django/core/signing.py

    From 9dda244bc6485d2c4eaea75793d3b51d682a8ae9 Mon Sep 17 00:00:00 2001
    From: Luke Plant <L.Plant.98@cantab.net>
    Date: Fri, 18 Feb 2022 12:26:41 +0000
    Subject: [PATCH] Improved docs for Signer to encourage more secure patterns.
    
    ---
     django/core/signing.py        |  8 +--
     docs/ref/request-response.txt | 17 ++++---
     docs/topics/signing.txt       | 96 +++++++++++++++++++++++++++++------
     3 files changed, 94 insertions(+), 27 deletions(-)
    
    diff --git a/django/core/signing.py b/django/core/signing.py
    index 6e284f4c84..43bdc2efd5 100644
    a b  
    33
    44The format used looks like this:
    55
    6 >>> signing.dumps("hello")
     6>>> signing.dumps("hello", salt="purpose1")
    77'ImhlbGxvIg:1QaUZC:YIye-ze3TTx7gtSv422nZA4sgmk'
    88
    99There are two components here, separated by a ':'. The first component is a
    1010URLsafe base64 encoded JSON of the object passed to dumps(). The second
    1111component is a base64 encoded hmac/SHA-256 hash of "$first_component:$secret"
    1212
    1313signing.loads(s) checks the signature and returns the deserialized object.
    1414If the signature fails, a BadSignature exception is raised.
    1515
    16 >>> signing.loads("ImhlbGxvIg:1QaUZC:YIye-ze3TTx7gtSv422nZA4sgmk")
     16>>> signing.loads("ImhlbGxvIg:1QaUZC:YIye-ze3TTx7gtSv422nZA4sgmk", salt="purpose1")
    1717'hello'
    18 >>> signing.loads("ImhlbGxvIg:1QaUZC:YIye-ze3TTx7gtSv42-modified")
     18>>> signing.loads("ImhlbGxvIg:1QaUZC:YIye-ze3TTx7gtSv42-modified", salt="purpose1")
    1919...
    2020BadSignature: Signature "ImhlbGxvIg:1QaUZC:YIye-ze3TTx7gtSv42-modified" does
    2121not match
    2222
    2323You can optionally compress the JSON prior to base64 encoding it to save
    2424space, using the compress=True argument. This checks if compression actually
    2525helps and only applies compression if the result is a shorter string:
    2626
    27 >>> signing.dumps(list(range(1, 20)), compress=True)
     27>>> signing.dumps(list(range(1, 20)), salt="purpose1", compress=True)
    2828'.eJwFwcERACAIwLCF-rCiILN47r-GyZVJsNgkxaFxoDgxcOHGxMKD_T7vhAml:1QaUaL:BA0thEZrp4FQVXIXuOvYJtLJSrQ'
    2929
    3030The fact that the string is compressed is signalled by the prefixed '.' at the
  • docs/ref/request-response.txt

    diff --git a/docs/ref/request-response.txt b/docs/ref/request-response.txt
    index 5469562a2d..c39a2bef5d 100644
    a b Methods  
    10651065
    10661066.. method:: HttpResponse.set_signed_cookie(key, value, salt='', max_age=None, expires=None, path='/', domain=None, secure=False, httponly=False, samesite=None)
    10671067
    1068     Like :meth:`~HttpResponse.set_cookie`, but
    1069     :doc:`cryptographic signing </topics/signing>` the cookie before setting
    1070     it. Use in conjunction with :meth:`HttpRequest.get_signed_cookie`.
    1071     You can use the optional ``salt`` argument to put the cookie into a
    1072     separate signature namespace, but you will need to remember to pass it to
    1073     the corresponding
    1074     :meth:`HttpRequest.get_signed_cookie` call.
     1068    Like :meth:`~HttpResponse.set_cookie`, but uses :doc:`cryptographic
     1069    signing </topics/signing>` on the cookie value before setting it. Use in
     1070    conjunction with :meth:`HttpRequest.get_signed_cookie`.
     1071
     1072    The ``key`` is used to generate the signature, to stop attackers re-using a
     1073    value signed for one key as a valid value for a different key. In addition,
     1074    it is helpful to use a unique ``salt`` argument for every different purpose
     1075    you use signed cookies for. See :ref:`understanding-signing-salt` for more
     1076    details. You will need to remember to pass the same ``salt`` value to the
     1077    corresponding :meth:`HttpRequest.get_signed_cookie` call.
    10751078
    10761079.. method:: HttpResponse.delete_cookie(key, path='/', domain=None, samesite=None)
    10771080
  • docs/topics/signing.txt

    diff --git a/docs/topics/signing.txt b/docs/topics/signing.txt
    index 57370fa4bb..cd79107fff 100644
    a b generate signatures. You can use a different secret by passing it to the  
    120120    of additional values used to validate signed data, defaults to
    121121    :setting:`SECRET_KEY_FALLBACKS`.
    122122
    123 Using the ``salt`` argument
    124 ---------------------------
     123.. _understanding-signing-salt:
    125124
    126 If you do not wish for every occurrence of a particular string to have the same
    127 signature hash, you can use the optional ``salt`` argument to the ``Signer``
    128 class. Using a salt will seed the signing hash function with both the salt and
    129 your :setting:`SECRET_KEY`:
     125Understanding and using the ``salt`` argument
     126---------------------------------------------
     127
     128Every different purpose for which you use signing should either have a
     129different secret (which is passed as the first argument to ``Signer`` and is
     130equal to :setting:`SECRET_KEY` by default), or use a unique ``salt`` argument
     131or both.
     132
     133Using a salt will seed the signing hash function with both the salt and
     134your secret:
    130135
    131136.. code-block:: pycon
    132137
    133     >>> signer = Signer()
     138    >>> signer = Signer(salt="myproject.purpose1")
    134139    >>> signer.sign("My string")
    135     'My string:v9G-nxfz3iQGTXrePqYPlGvH79WTcIgj1QIQSUODTW0'
     140    'My string:vcazRX-FXPKIWyPdTTVn1pD3S4C3afjW9WkQB2R3Z9A'
    136141    >>> signer.sign_object({"message": "Hello!"})
    137     'eyJtZXNzYWdlIjoiSGVsbG8hIn0:bzb48DBkB-bwLaCnUVB75r5VAPUEpzWJPrTb80JMIXM'
    138     >>> signer = Signer(salt="extra")
     142    'eyJtZXNzYWdlIjoiSGVsbG8hIn0:vbmnwn5TK03_JZ-4PR8IHeBPhWaY8nU5C0FOr_TVXnA'
     143    >>> signer = Signer(salt="myproject.purpose2")
    139144    >>> signer.sign("My string")
    140     'My string:YMD-FR6rof3heDkFRffdmG4pXbAZSOtb-aQxg3vmmfc'
    141     >>> signer.unsign("My string:YMD-FR6rof3heDkFRffdmG4pXbAZSOtb-aQxg3vmmfc")
     145    'My string:62ZfWuX41GqG_BOKYQq4vYB58G5pYO0h8tvM0OyCbyk'
     146    >>> signer.unsign("My string:62ZfWuX41GqG_BOKYQq4vYB58G5pYO0h8tvM0OyCbyk")
    142147    'My string'
    143148    >>> signer.sign_object({"message": "Hello!"})
    144     'eyJtZXNzYWdlIjoiSGVsbG8hIn0:-UWSLCE-oUAHzhkHviYz3SOZYBjFKllEOyVZNuUtM-I'
     149    'eyJtZXNzYWdlIjoiSGVsbG8hIn0:w-C2ISTdMDgvexMJlbslLXOp0lNVA1zQtOOyAkkvBlQ'
    145150    >>> signer.unsign_object(
    146     ...     "eyJtZXNzYWdlIjoiSGVsbG8hIn0:-UWSLCE-oUAHzhkHviYz3SOZYBjFKllEOyVZNuUtM-I"
     151    ...     "eyJtZXNzYWdlIjoiSGVsbG8hIn0:w-C2ISTdMDgvexMJlbslLXOp0lNVA1zQtOOyAkkvBlQ"
    147152    ... )
    148153    {'message': 'Hello!'}
    149154
    150155Using salt in this way puts the different signatures into different
    151156namespaces. A signature that comes from one namespace (a particular salt
    152157value) cannot be used to validate the same plaintext string in a different
    153158namespace that is using a different salt setting. The result is to prevent an
    154159attacker from using a signed string generated in one place in the code as input
    155 to another piece of code that is generating (and verifying) signatures using a
    156 different salt.
     160to another piece of code that is generating (and verifying) signatures for a
     161different purpose or in a different context.
    157162
    158163Unlike your :setting:`SECRET_KEY`, your salt argument does not need to stay
    159164secret.
    160165
     166In addition to using salt values, you should think carefully about the value
     167you are signing, and how it might be re-used by an attacker.
     168
     169For example, suppose you are an insurance company, and give a user a quote for
     170home insurance of $20/month. You send the user a signed value generated as
     171follows, perhaps stored in a cookie:
     172
     173.. code-block:: pycon
     174
     175    >>> signer = Signer(salt="acmeinsurance.quotes")
     176    >>> signer.sign("20")
     177
     178
     179There are multiple issues with this:
     180
     1811. It has no time limit or timestamp, so the value can be used forever.
     182
     1832. We are simply signing the string ``"20"`` which is very ambiguous in
     184   meaning. You might also sell car insurance, and an attacker would be able to
     185   take this signed value that was intended for home insurance and re-use it to
     186   get cheap car insurance. Or they might re-use it for a completely different
     187   house, or re-use it in a context where it means a yearly amount, rather than
     188   a monthly amount.
     189
     1903. There is no scoping to the user. The user could take this signed value and
     191   share it with a friend, who would also be able to use it on your site.
     192
     193The answers to these problems are:
     194
     195- Use :class:`~TimestampSigner`, or build an expiration timestamp into the
     196  value that you sign, which you check later.
     197
     198- Use a more specific salt.
     199
     200- Instead of signing simple values without context, sign complex objects that
     201  express more completely what you want to sign - “we offer the user with email
     202  address ``user@example.com`` home insurance for 123 Main St, for the price of
     203  20 dollars a month, valid until 2022-05-01”:
     204
     205  .. code-block:: pycon
     206
     207      >>> signer = Signer(salt="acmeinsurance.quotes.home_insurance")
     208      >>> signer.sign_object(
     209      ...     {
     210      ...         "user_email": "user@example.com",
     211      ...         "address": "123 Main St, 9980-999",
     212      ...         "amount": 20,
     213      ...         "unit": "dollars",
     214      ...         "period": "month",
     215      ...         "valid_until": "2022-05-01",
     216      ...     }
     217      ... )
     218
     219- Make sure you use or check each part of the value returned after calling
     220  ``unsign``, rather than assuming it matches what you expect.
     221
     222- Instead of doing this at all, store the values in the database and give
     223  the user just a reference.
     224
    161225Verifying timestamped values
    162226----------------------------
    163227
Back to Top