Code

Ticket #16199: 16199.5.diff

File 16199.5.diff, 10.5 KB (added by jezdez, 3 years ago)

moar warnings

Line 
1diff --git a/django/contrib/sessions/backends/cookies.py b/django/contrib/sessions/backends/cookies.py
2new file mode 100644
3index 0000000..bff221b
4--- /dev/null
5+++ b/django/contrib/sessions/backends/cookies.py
6@@ -0,0 +1,90 @@
7+import zlib
8+try:
9+    import cPickle as pickle
10+except ImportError:
11+    import pickle
12+
13+from django.conf import settings
14+from django.core import signing
15+from django.utils.encoding import smart_str
16+
17+from django.contrib.sessions.backends.base import SessionBase
18+
19+
20+class SessionStore(SessionBase):
21+    salt = 'django.contrib.sessions.backends.cookies'
22+
23+    def load(self):
24+        """
25+        We load the data from the key itself instead of fetching from
26+        some external data store. Opposite of _get_session_key(),
27+        raises BadSignature if signature fails.
28+        """
29+        signer = signing.TimestampSigner(salt=self.salt)
30+        try:
31+            base64d = signer.unsign(
32+                self._session_key, max_age=settings.SESSION_COOKIE_AGE)
33+            pickled = signing.b64_decode(smart_str(base64d))
34+            return pickle.loads(zlib.decompress(pickled))
35+        except (signing.BadSignature, ValueError):
36+            self.create()
37+            return {}
38+
39+    def create(self):
40+        """
41+        To create a new key, we simply make sure that the modified flag is set
42+        so that the cookie is set on the client for the current request.
43+        """
44+        self.modified = True
45+
46+    def save(self):
47+        """
48+        To save, we get the session key as a securely signed string and then
49+        set the modified flag so that the cookie is set on the client for the
50+        current request.
51+        """
52+        self._session_key = self._get_session_key()
53+        self.modified = True
54+
55+    def exists(self, session_key=None):
56+        """
57+        This method makes sense when you're talking to a shared resource, but
58+        it doesn't matter when you're storing the information in the client's
59+        cookie.
60+        """
61+        return False
62+
63+    def delete(self, session_key=None):
64+        """
65+        To delete, we clear the session key and the underlying data structure
66+        and set the modified flag so that the cookie is set on the client for
67+        the current request.
68+        """
69+        self._session_key = ''
70+        self._session_cache = {}
71+        self.modified = True
72+
73+    def cycle_key(self):
74+        """
75+        Keeps the same data but with a new key.  To do this, we just have to
76+        call ``save()`` and it will automatically save a cookie with a new key
77+        at the end of the request.
78+        """
79+        self.save()
80+
81+    def _get_session_key(self):
82+        """
83+        Most session backends don't need to override this method, but we do,
84+        because instead of generating a random string, we want to actually
85+        generate a secure url-safe Base64-encoded string of data as our
86+        session key.
87+        """
88+        payload = getattr(self, '_session_cache', {})
89+        pickled = pickle.dumps(payload, pickle.HIGHEST_PROTOCOL)
90+        base64d = signing.b64_encode(zlib.compress(pickled))
91+        return signing.TimestampSigner(salt=self.salt).sign(base64d)
92+
93+    def _set_session_key(self, session_key):
94+        self._session_key = session_key
95+
96+    session_key = property(_get_session_key, _set_session_key)
97diff --git a/django/contrib/sessions/tests.py b/django/contrib/sessions/tests.py
98index 2eb43f3..0c34101 100644
99--- a/django/contrib/sessions/tests.py
100+++ b/django/contrib/sessions/tests.py
101@@ -7,11 +7,13 @@ from django.contrib.sessions.backends.db import SessionStore as DatabaseSession
102 from django.contrib.sessions.backends.cache import SessionStore as CacheSession
103 from django.contrib.sessions.backends.cached_db import SessionStore as CacheDBSession
104 from django.contrib.sessions.backends.file import SessionStore as FileSession
105+from django.contrib.sessions.backends.cookies import SessionStore as CookieSession
106 from django.contrib.sessions.models import Session
107 from django.contrib.sessions.middleware import SessionMiddleware
108 from django.core.exceptions import ImproperlyConfigured, SuspiciousOperation
109 from django.http import HttpResponse
110 from django.test import TestCase, RequestFactory
111+from django.test.utils import override_settings
112 from django.utils import unittest
113 
114 
115@@ -214,10 +216,7 @@ class SessionTestsMixin(object):
116         # Tests get_expire_at_browser_close with different settings and different
117         # set_expiry calls
118         try:
119-            try:
120-                original_expire_at_browser_close = settings.SESSION_EXPIRE_AT_BROWSER_CLOSE
121-                settings.SESSION_EXPIRE_AT_BROWSER_CLOSE = False
122-
123+            with override_settings(SESSION_EXPIRE_AT_BROWSER_CLOSE=False):
124                 self.session.set_expiry(10)
125                 self.assertFalse(self.session.get_expire_at_browser_close())
126 
127@@ -227,8 +226,7 @@ class SessionTestsMixin(object):
128                 self.session.set_expiry(None)
129                 self.assertFalse(self.session.get_expire_at_browser_close())
130 
131-                settings.SESSION_EXPIRE_AT_BROWSER_CLOSE = True
132-
133+            with override_settings(SESSION_EXPIRE_AT_BROWSER_CLOSE=True):
134                 self.session.set_expiry(10)
135                 self.assertFalse(self.session.get_expire_at_browser_close())
136 
137@@ -237,11 +235,8 @@ class SessionTestsMixin(object):
138 
139                 self.session.set_expiry(None)
140                 self.assertTrue(self.session.get_expire_at_browser_close())
141-
142-            except:
143-                raise
144-        finally:
145-            settings.SESSION_EXPIRE_AT_BROWSER_CLOSE = original_expire_at_browser_close
146+        except:
147+            raise
148 
149     def test_decode(self):
150         # Ensure we can decode what we encode
151@@ -302,9 +297,10 @@ class FileSessionTests(SessionTestsMixin, unittest.TestCase):
152         shutil.rmtree(self.temp_session_store)
153         super(FileSessionTests, self).tearDown()
154 
155+    @override_settings(
156+        SESSION_FILE_PATH="/if/this/directory/exists/you/have/a/weird/computer")
157     def test_configuration_check(self):
158         # Make sure the file backend checks for a good storage dir
159-        settings.SESSION_FILE_PATH = "/if/this/directory/exists/you/have/a/weird/computer"
160         self.assertRaises(ImproperlyConfigured, self.backend)
161 
162     def test_invalid_key_backslash(self):
163@@ -324,17 +320,9 @@ class CacheSessionTests(SessionTestsMixin, unittest.TestCase):
164 
165 
166 class SessionMiddlewareTests(unittest.TestCase):
167-    def setUp(self):
168-        self.old_SESSION_COOKIE_SECURE = settings.SESSION_COOKIE_SECURE
169-        self.old_SESSION_COOKIE_HTTPONLY = settings.SESSION_COOKIE_HTTPONLY
170-
171-    def tearDown(self):
172-        settings.SESSION_COOKIE_SECURE = self.old_SESSION_COOKIE_SECURE
173-        settings.SESSION_COOKIE_HTTPONLY = self.old_SESSION_COOKIE_HTTPONLY
174 
175+    @override_settings(SESSION_COOKIE_SECURE=True)
176     def test_secure_session_cookie(self):
177-        settings.SESSION_COOKIE_SECURE = True
178-
179         request = RequestFactory().get('/')
180         response = HttpResponse('Session test')
181         middleware = SessionMiddleware()
182@@ -347,9 +335,8 @@ class SessionMiddlewareTests(unittest.TestCase):
183         response = middleware.process_response(request, response)
184         self.assertTrue(response.cookies[settings.SESSION_COOKIE_NAME]['secure'])
185 
186+    @override_settings(SESSION_COOKIE_HTTPONLY=True)
187     def test_httponly_session_cookie(self):
188-        settings.SESSION_COOKIE_HTTPONLY = True
189-
190         request = RequestFactory().get('/')
191         response = HttpResponse('Session test')
192         middleware = SessionMiddleware()
193@@ -361,3 +348,15 @@ class SessionMiddlewareTests(unittest.TestCase):
194         # Handle the response through the middleware
195         response = middleware.process_response(request, response)
196         self.assertTrue(response.cookies[settings.SESSION_COOKIE_NAME]['httponly'])
197+
198+
199+class CookieSessionTests(SessionTestsMixin, TestCase):
200+
201+    backend = CookieSession
202+
203+    def test_save(self):
204+        """
205+        This test tested exists() in the other session backends, but that
206+        doesn't make sense for us.
207+        """
208+        pass
209diff --git a/django/core/signing.py b/django/core/signing.py
210index 3165cf8..224d942 100644
211--- a/django/core/signing.py
212+++ b/django/core/signing.py
213@@ -161,7 +161,7 @@ class TimestampSigner(Signer):
214     def __init__(self, *args, **kwargs):
215         self.time_func = kwargs.pop('time', time.time)
216         super(TimestampSigner, self).__init__(*args, **kwargs)
217-   
218+
219     def timestamp(self):
220         return baseconv.base62.encode(int(self.time_func() * 10000))
221 
222diff --git a/docs/topics/http/sessions.txt b/docs/topics/http/sessions.txt
223index 8529f53..9d19acc 100644
224--- a/docs/topics/http/sessions.txt
225+++ b/docs/topics/http/sessions.txt
226@@ -95,6 +95,33 @@ defaults to output from ``tempfile.gettempdir()``, most likely ``/tmp``) to
227 control where Django stores session files. Be sure to check that your Web
228 server has permissions to read and write to this location.
229 
230+Using cookie-based sessions
231+---------------------------
232+
233+.. versionadded:: 1.4
234+
235+To use cookies-based sessions, set the :setting:`SESSION_ENGINE` setting to
236+``"django.contrib.sessions.backends.cookies"``. The session data will be
237+stored using Django's tools for :doc:`cryptographic signing </topics/signing>`
238+and the :setting:`SECRET_KEY` setting. It's recommended to set the
239+:setting:`SESSION_COOKIE_HTTPONLY` to ``True`` to prevent tampering from
240+JavaScript.
241+
242+.. warning::
243+
244+    The session data is **signed but not encrypted**!
245+
246+    When using the cookies backend the session data can be read out
247+    and will be invalidated when being tampered with. The same invalidation
248+    happens if the client storing the cookie (e.g. your user's browser)
249+    can't store all of the session cookie and drops data. Even though
250+    Django compresses the data before it's still entirely possible to
251+    exceed the `common limit of 4096 bytes`_ per cookie.
252+
253+    Also, the size of a cookie can have an impact on the `speed of your site`_.
254+
255+.. _`common limit of 4096 bytes`: http://tools.ietf.org/html/rfc2965#section-5.3
256+.. _`speed of your site`: http://yuiblog.com/blog/2007/03/01/performance-research-part-3/
257 
258 Using sessions in views
259 =======================
260@@ -420,6 +447,7 @@ Controls where Django stores session data. Valid values are:
261     * ``'django.contrib.sessions.backends.file'``
262     * ``'django.contrib.sessions.backends.cache'``
263     * ``'django.contrib.sessions.backends.cached_db'``
264+    * ``'django.contrib.sessions.backends.cookies'``
265 
266 See `configuring the session engine`_ for more details.
267