Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Improvements to contrib.sessions #78

Closed
wants to merge 7 commits into from

3 participants

Rohan Jain Alex Ogier Preston Holmes
Rohan Jain
crodjer commented May 18, 2012
  • Check session expiry using the sigining framework
  • Extend the session key character set
  • Cleanup management command improvements
Alex Ogier
ogier commented May 22, 2012

Doesn't the fallback negate any security benefit to signing? If an attacker could break the old (insecure) mechanism, they can now break yours by triggering the old mechanism.

Rohan Jain
crodjer commented May 22, 2012
added some commits May 18, 2012
Rohan Jain Add myself to authors
Signed-off-by: Rohan Jain <crodjer@gmail.com>
ce27fe1
Rohan Jain Check session expiry on the serve side
Use timed signer to check for expiration of session data. This is to
fix ticket #18194. The sessions based on file backend otherwise do not
expire, as far as the server is concerned.

Signed-off-by: Rohan Jain <crodjer@gmail.com>
3b018b6
Rohan Jain
crodjer commented May 28, 2012

To remove merge commits from the pull request, I did a rebase and re-added the commits in this pull request. In that process, I lost some comments on the commit "Check session expiry on the serve side", Line 79 . Here are those:

On 19:57 -0700 / 21 May, Sergiy Kuzmenko (@shelldweller) wrote:

2 compatibility issues:

1) This will invalidate all existing sessions that were created the old way (and will likely throw an uncaught exception).
2) Exception change for tempered data: SuspiciousOperation is implicitly replaced by BadSignature. (This might be the right thing to do but it must be documented).

On 12:02 +0530 / 22 May, Rohan Jain (@crodjer) wrote:

In case of an exception while unsigning existing sessions, we can fall
back to the previous decoding method. Added a commit for this in the
pull request.

added some commits May 18, 2012
Rohan Jain Extend session key char set
Signed-off-by: Rohan Jain <crodjer@gmail.com>
f5700b9
Rohan Jain Session cleanup management command improvements
Cleanup logic now lies in the backend. It will be executed based on
the currently set backend.
Adds a cleanup functionality for the file backend and db backend.

Signed-off-by: Rohan Jain <crodjer@gmail.com>
3f2e5ee
Rohan Jain Remove unused imports
Signed-off-by: Rohan Jain <crodjer@gmail.com>
877edf6
Rohan Jain Compatibility decoding of existing sessions
The existing sessions, which were not signed with the signing
framework is handled with the older decoding method.
Mark the session as modified so that it uses the new encoding method
for storing the data.

Signed-off-by: Rohan Jain <crodjer@gmail.com>
2f46173
Rohan Jain Make compatibility with older mechanism optional
Don't enable compatibility with older mechanism by default as it
compromises with the security benefits of introducing signing
framework.

Signed-off-by: Rohan Jain <crodjer@gmail.com>
89b90a0
Preston Holmes
Owner

There seem to be several tickets worth of stuff in here any references to open tickets?

This at least seems related to management stuff

https://code.djangoproject.com/ticket/18978

Rohan Jain

Yes and the initial commits here are for session expiry related issues: https://code.djangoproject.com/ticket/18194

Preston Holmes
Owner

There is some good work here - but please refactor it so that it is one pull request per ticket - and then cross reference the tickets and pulls to each other so that reviewers can find and connect them.

Thanks!

Preston Holmes ptone closed this October 06, 2012
Rohan Jain

Sure, I'll do that as soon as possible.

Thanks

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Showing 7 unique commits by 1 author.

May 29, 2012
Rohan Jain Add myself to authors
Signed-off-by: Rohan Jain <crodjer@gmail.com>
ce27fe1
Rohan Jain Check session expiry on the serve side
Use timed signer to check for expiration of session data. This is to
fix ticket #18194. The sessions based on file backend otherwise do not
expire, as far as the server is concerned.

Signed-off-by: Rohan Jain <crodjer@gmail.com>
3b018b6
Jun 04, 2012
Rohan Jain Extend session key char set
Signed-off-by: Rohan Jain <crodjer@gmail.com>
f5700b9
Rohan Jain Session cleanup management command improvements
Cleanup logic now lies in the backend. It will be executed based on
the currently set backend.
Adds a cleanup functionality for the file backend and db backend.

Signed-off-by: Rohan Jain <crodjer@gmail.com>
3f2e5ee
Rohan Jain Remove unused imports
Signed-off-by: Rohan Jain <crodjer@gmail.com>
877edf6
Rohan Jain Compatibility decoding of existing sessions
The existing sessions, which were not signed with the signing
framework is handled with the older decoding method.
Mark the session as modified so that it uses the new encoding method
for storing the data.

Signed-off-by: Rohan Jain <crodjer@gmail.com>
2f46173
Rohan Jain Make compatibility with older mechanism optional
Don't enable compatibility with older mechanism by default as it
compromises with the security benefits of introducing signing
framework.

Signed-off-by: Rohan Jain <crodjer@gmail.com>
89b90a0
This page is out of date. Refresh to see the latest.
1  AUTHORS
@@ -566,6 +566,7 @@ answer newbie questions, and generally made Django that much better:
566 566
     Gasper Zejn <zejn@kiberpipa.org>
567 567
     Jarek Zgoda <jarek.zgoda@gmail.com>
568 568
     Cheng Zhang
  569
+    Rohan Jain <crodjer@gmail.com>
569 570
 
570 571
 A big THANK YOU goes to:
571 572
 
57  django/contrib/sessions/backends/base.py
... ...
@@ -1,6 +1,6 @@
1 1
 import base64
2  
-import time
3 2
 from datetime import datetime, timedelta
  3
+
4 4
 try:
5 5
     import cPickle as pickle
6 6
 except ImportError:
@@ -8,10 +8,11 @@
8 8
 
9 9
 from django.conf import settings
10 10
 from django.core.exceptions import SuspiciousOperation
11  
-from django.utils.crypto import constant_time_compare
12 11
 from django.utils.crypto import get_random_string
13 12
 from django.utils.crypto import salted_hmac
  13
+from django.utils.crypto import constant_time_compare
14 14
 from django.utils import timezone
  15
+from django.core import signing
15 16
 
16 17
 class CreateError(Exception):
17 18
     """
@@ -27,10 +28,15 @@ class SessionBase(object):
27 28
     TEST_COOKIE_NAME = 'testcookie'
28 29
     TEST_COOKIE_VALUE = 'worked'
29 30
 
  31
+    # Session_key should not be case sensitive because some backends can store
  32
+    # it on case insensitive file systems.
  33
+    VALID_KEY_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789"
  34
+
30 35
     def __init__(self, session_key=None):
31 36
         self._session_key = session_key
32 37
         self.accessed = False
33 38
         self.modified = False
  39
+        self.signer = signing.TimestampSigner()
34 40
 
35 41
     def __contains__(self, key):
36 42
         return key in self._session
@@ -76,11 +82,43 @@ def _hash(self, value):
76 82
 
77 83
     def encode(self, session_dict):
78 84
         "Returns the given session dictionary pickled and encoded as a string."
  85
+        serialized = pickle.dumps(session_dict)
  86
+        return self.signer.sign(serialized)
  87
+
  88
+    def decode(self, session_data):
  89
+        try:
  90
+            try:
  91
+                serialized = str(self.signer.unsign(session_data,
  92
+                                                    settings.SESSION_COOKIE_AGE))
  93
+
  94
+            except signing.SignatureExpired:
  95
+                # An expired signature means it is not a compatibility issue so
  96
+                # raise it.
  97
+                raise
  98
+
  99
+            except signing.BadSignature:
  100
+                if getattr(settings, 'SESSION_KEEP_COMPATIBLE'):
  101
+                    return self.decode_legacy(session_data)
  102
+                else:
  103
+                    raise
  104
+
  105
+            return pickle.loads(serialized)
  106
+
  107
+        except Exception:
  108
+            # ValueError, SuspiciousOperation, BadSignature, unpickling
  109
+            # exceptions. If any of these happen, just return an empty
  110
+            # dictionary (an empty session).
  111
+            self.modified = True
  112
+            return {}
  113
+
  114
+    def encode_legacy(self, session_dict):
  115
+        "Encoding mechanism used earlier. Deprecated"
79 116
         pickled = pickle.dumps(session_dict, pickle.HIGHEST_PROTOCOL)
80 117
         hash = self._hash(pickled)
81 118
         return base64.encodestring(hash + ":" + pickled)
82 119
 
83  
-    def decode(self, session_data):
  120
+    def decode_legacy(self, session_data):
  121
+        "Decoding mechanism used earlier. Deprecated"
84 122
         encoded_data = base64.decodestring(session_data)
85 123
         try:
86 124
             # could produce ValueError if there is no ':'
@@ -130,12 +168,8 @@ def clear(self):
130 168
 
131 169
     def _get_new_session_key(self):
132 170
         "Returns session key that isn't being used."
133  
-        # Todo: move to 0-9a-z charset in 1.5
134  
-        hex_chars = '1234567890abcdef'
135  
-        # session_key should not be case sensitive because some backends
136  
-        # can store it on case insensitive file systems.
137 171
         while True:
138  
-            session_key = get_random_string(32, hex_chars)
  172
+            session_key = get_random_string(32, self.VALID_KEY_CHARS)
139 173
             if not self.exists(session_key):
140 174
                 break
141 175
         return session_key
@@ -278,3 +312,10 @@ def load(self):
278 312
         Loads the session data and returns a dictionary.
279 313
         """
280 314
         raise NotImplementedError
  315
+
  316
+    @classmethod
  317
+    def cleanup(cls):
  318
+        """
  319
+        Cleaunp the expired sessions
  320
+        """
  321
+        raise NotImplementedError
4  django/contrib/sessions/backends/db.py
@@ -72,6 +72,10 @@ def delete(self, session_key=None):
72 72
         except Session.DoesNotExist:
73 73
             pass
74 74
 
  75
+    @classmethod
  76
+    def cleanup(cls):
  77
+        Session.objects.filter(expire_date__lt=timezone.now()).delete()
  78
+        transaction.commit_unless_managed()
75 79
 
76 80
 # At bottom to avoid circular import
77 81
 from django.contrib.sessions.models import Session
19  django/contrib/sessions/backends/file.py
@@ -26,8 +26,6 @@ def __init__(self, session_key=None):
26 26
         self.file_prefix = settings.SESSION_COOKIE_NAME
27 27
         super(SessionStore, self).__init__(session_key)
28 28
 
29  
-    VALID_KEY_CHARS = set("abcdef0123456789")
30  
-
31 29
     def _key_to_file(self, session_key=None):
32 30
         """
33 31
         Get the file associated with this session key.
@@ -38,7 +36,7 @@ def _key_to_file(self, session_key=None):
38 36
         # Make sure we're not vulnerable to directory traversal. Session keys
39 37
         # should always be md5s, so they should never contain directory
40 38
         # components.
41  
-        if not set(session_key).issubset(self.VALID_KEY_CHARS):
  39
+        if not set(session_key).issubset(set(self.VALID_KEY_CHARS)):
42 40
             raise SuspiciousOperation(
43 41
                 "Invalid characters in session key")
44 42
 
@@ -142,3 +140,18 @@ def delete(self, session_key=None):
142 140
 
143 141
     def clean(self):
144 142
         pass
  143
+
  144
+    @classmethod
  145
+    def cleanup(cls):
  146
+        storage_path = getattr(settings, "SESSION_FILE_PATH", tempfile.gettempdir())
  147
+        file_prefix = settings.SESSION_COOKIE_NAME
  148
+
  149
+        # Get all file sessions stored
  150
+        sessions = [cls(session_file[len(file_prefix):])
  151
+                        for session_file in os.listdir(storage_path)
  152
+                        if session_file.startswith(file_prefix)]
  153
+
  154
+        # Cleanup all empty sessions
  155
+        for session in sessions:
  156
+            if not session.load():
  157
+                session.delete()
37  django/contrib/sessions/tests.py
... ...
@@ -1,4 +1,4 @@
1  
-from datetime import datetime, timedelta
  1
+from datetime import timedelta
2 2
 import shutil
3 3
 import string
4 4
 import tempfile
@@ -253,6 +253,26 @@ def test_decode(self):
253 253
         encoded = self.session.encode(data)
254 254
         self.assertEqual(self.session.decode(encoded), data)
255 255
 
  256
+    def test_decode_legacy(self):
  257
+        # Ensure we can decode what we encode
  258
+        data = {'a test key': 'a test value'}
  259
+        encoded = self.session.encode_legacy(data)
  260
+        self.assertEqual(self.session.decode_legacy(encoded), data)
  261
+
  262
+    def test_decode_compatibility_disabled(self):
  263
+        # Test that session data encoded with legacy mechanisms is reset when
  264
+        # compatibility is disabled
  265
+        data = {'a test key': 'a test value'}
  266
+        encoded = self.session.encode_legacy(data)
  267
+        self.assertEqual(self.session.decode(encoded), {})
  268
+
  269
+    @override_settings(SESSION_KEEP_COMPATIBLE=True)
  270
+    def test_decode_compatibility_enabled(self):
  271
+        # Test that session data encoded with legacy mechanisms is not reset
  272
+        # when compatibility is enabled
  273
+        data = {'a test key': 'a test value'}
  274
+        encoded = self.session.encode_legacy(data)
  275
+        self.assertEqual(self.session.decode(encoded), data)
256 276
 
257 277
 class DatabaseSessionTests(SessionTestsMixin, TestCase):
258 278
 
@@ -345,6 +365,21 @@ def test_invalid_key_forwardslash(self):
345 365
         self.assertRaises(SuspiciousOperation,
346 366
                           self.backend("a/b/c").load)
347 367
 
  368
+    # This test fails with cookie (which is fine I suppose) and cache backends,
  369
+    # thats why added it to file tests only.
  370
+    @override_settings(SESSION_COOKIE_AGE=0)
  371
+    def test_onload_expiry_check(self):
  372
+        """
  373
+        Test to ensure that expiry of session is checked on-load
  374
+        """
  375
+
  376
+        # Setup a test cookie
  377
+        self.session.set_test_cookie()
  378
+        self.assertTrue(self.session.test_cookie_worked())
  379
+
  380
+        self.session.load()
  381
+        # The test data should be absent now, as the cookie age is 0
  382
+        self.assertFalse(self.session.test_cookie_worked())
348 383
 
349 384
 class CacheSessionTests(SessionTestsMixin, unittest.TestCase):
350 385
 
9  django/core/management/commands/cleanup.py
... ...
@@ -1,11 +1,10 @@
1 1
 from django.core.management.base import NoArgsCommand
2  
-from django.utils import timezone
  2
+from django.utils.importlib import import_module
  3
+from django.conf import settings
3 4
 
4 5
 class Command(NoArgsCommand):
5 6
     help = "Can be run as a cronjob or directly to clean out old data from the database (only expired sessions at the moment)."
6 7
 
7 8
     def handle_noargs(self, **options):
8  
-        from django.db import transaction
9  
-        from django.contrib.sessions.models import Session
10  
-        Session.objects.filter(expire_date__lt=timezone.now()).delete()
11  
-        transaction.commit_unless_managed()
  9
+        engine = import_module(settings.SESSION_ENGINE)
  10
+        engine.SessionStore.cleanup()
17  docs/topics/http/sessions.txt
@@ -568,6 +568,23 @@ Whether to save the session data on every request. If this is ``False``
568 568
 (default), then the session data will only be saved if it has been modified --
569 569
 that is, if any of its dictionary values have been assigned or deleted.
570 570
 
  571
+SESSION_KEEP_COMPATIBLE
  572
+-----------------------
  573
+
  574
+Default: ``False``
  575
+
  576
+Wheather to continue support for existing sessions, which were created with
  577
+older mechanism. This is useful if you want the existing sessions not to
  578
+invalidate. If set to True, the legacy sessions will be gradually converted to
  579
+the signed sessions, after that this setting can be set to ``False`` again.
  580
+
  581
+.. warning::
  582
+
  583
+    Setting this to ``True`` will make you susceptible to all the security risks
  584
+    from the previous mechanism which lacked data signing. Use this if you don't
  585
+    want all the users to automatically logout after a django upgrade.
  586
+
  587
+
571 588
 .. _Django settings: ../settings/
572 589
 
573 590
 Technical details
Commit_comment_tip

Tip: You can add notes to lines in a file. Hover to the left of a line to make a note

Something went wrong with that request. Please try again.