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 , 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 3 3 4 4 The format used looks like this: 5 5 6 >>> signing.dumps("hello" )6 >>> signing.dumps("hello", salt="purpose1") 7 7 'ImhlbGxvIg:1QaUZC:YIye-ze3TTx7gtSv422nZA4sgmk' 8 8 9 9 There are two components here, separated by a ':'. The first component is a 10 10 URLsafe base64 encoded JSON of the object passed to dumps(). The second 11 11 component is a base64 encoded hmac/SHA-256 hash of "$first_component:$secret" 12 12 13 13 signing.loads(s) checks the signature and returns the deserialized object. 14 14 If the signature fails, a BadSignature exception is raised. 15 15 16 >>> signing.loads("ImhlbGxvIg:1QaUZC:YIye-ze3TTx7gtSv422nZA4sgmk" )16 >>> signing.loads("ImhlbGxvIg:1QaUZC:YIye-ze3TTx7gtSv422nZA4sgmk", salt="purpose1") 17 17 'hello' 18 >>> signing.loads("ImhlbGxvIg:1QaUZC:YIye-ze3TTx7gtSv42-modified" )18 >>> signing.loads("ImhlbGxvIg:1QaUZC:YIye-ze3TTx7gtSv42-modified", salt="purpose1") 19 19 ... 20 20 BadSignature: Signature "ImhlbGxvIg:1QaUZC:YIye-ze3TTx7gtSv42-modified" does 21 21 not match 22 22 23 23 You can optionally compress the JSON prior to base64 encoding it to save 24 24 space, using the compress=True argument. This checks if compression actually 25 25 helps and only applies compression if the result is a shorter string: 26 26 27 >>> signing.dumps(list(range(1, 20)), compress=True)27 >>> signing.dumps(list(range(1, 20)), salt="purpose1", compress=True) 28 28 '.eJwFwcERACAIwLCF-rCiILN47r-GyZVJsNgkxaFxoDgxcOHGxMKD_T7vhAml:1QaUaL:BA0thEZrp4FQVXIXuOvYJtLJSrQ' 29 29 30 30 The 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 1065 1065 1066 1066 .. method:: HttpResponse.set_signed_cookie(key, value, salt='', max_age=None, expires=None, path='/', domain=None, secure=False, httponly=False, samesite=None) 1067 1067 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. 1075 1078 1076 1079 .. method:: HttpResponse.delete_cookie(key, path='/', domain=None, samesite=None) 1077 1080 -
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 120 120 of additional values used to validate signed data, defaults to 121 121 :setting:`SECRET_KEY_FALLBACKS`. 122 122 123 Using the ``salt`` argument 124 --------------------------- 123 .. _understanding-signing-salt: 125 124 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`: 125 Understanding and using the ``salt`` argument 126 --------------------------------------------- 127 128 Every different purpose for which you use signing should either have a 129 different secret (which is passed as the first argument to ``Signer`` and is 130 equal to :setting:`SECRET_KEY` by default), or use a unique ``salt`` argument 131 or both. 132 133 Using a salt will seed the signing hash function with both the salt and 134 your secret: 130 135 131 136 .. code-block:: pycon 132 137 133 >>> signer = Signer( )138 >>> signer = Signer(salt="myproject.purpose1") 134 139 >>> signer.sign("My string") 135 'My string:v 9G-nxfz3iQGTXrePqYPlGvH79WTcIgj1QIQSUODTW0'140 'My string:vcazRX-FXPKIWyPdTTVn1pD3S4C3afjW9WkQB2R3Z9A' 136 141 >>> 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") 139 144 >>> 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") 142 147 'My string' 143 148 >>> signer.sign_object({"message": "Hello!"}) 144 'eyJtZXNzYWdlIjoiSGVsbG8hIn0: -UWSLCE-oUAHzhkHviYz3SOZYBjFKllEOyVZNuUtM-I'149 'eyJtZXNzYWdlIjoiSGVsbG8hIn0:w-C2ISTdMDgvexMJlbslLXOp0lNVA1zQtOOyAkkvBlQ' 145 150 >>> signer.unsign_object( 146 ... "eyJtZXNzYWdlIjoiSGVsbG8hIn0: -UWSLCE-oUAHzhkHviYz3SOZYBjFKllEOyVZNuUtM-I"151 ... "eyJtZXNzYWdlIjoiSGVsbG8hIn0:w-C2ISTdMDgvexMJlbslLXOp0lNVA1zQtOOyAkkvBlQ" 147 152 ... ) 148 153 {'message': 'Hello!'} 149 154 150 155 Using salt in this way puts the different signatures into different 151 156 namespaces. A signature that comes from one namespace (a particular salt 152 157 value) cannot be used to validate the same plaintext string in a different 153 158 namespace that is using a different salt setting. The result is to prevent an 154 159 attacker 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 usinga156 different salt.160 to another piece of code that is generating (and verifying) signatures for a 161 different purpose or in a different context. 157 162 158 163 Unlike your :setting:`SECRET_KEY`, your salt argument does not need to stay 159 164 secret. 160 165 166 In addition to using salt values, you should think carefully about the value 167 you are signing, and how it might be re-used by an attacker. 168 169 For example, suppose you are an insurance company, and give a user a quote for 170 home insurance of $20/month. You send the user a signed value generated as 171 follows, perhaps stored in a cookie: 172 173 .. code-block:: pycon 174 175 >>> signer = Signer(salt="acmeinsurance.quotes") 176 >>> signer.sign("20") 177 178 179 There are multiple issues with this: 180 181 1. It has no time limit or timestamp, so the value can be used forever. 182 183 2. 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 190 3. 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 193 The 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 161 225 Verifying timestamped values 162 226 ---------------------------- 163 227