Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Improvements to contrib.sessions #78

Closed
wants to merge 7 commits into from

3 participants

@crodjer
  • Check session expiry using the sigining framework
  • Extend the session key character set
  • Cleanup management command improvements
@ogier

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.

@crodjer
crodjer added some commits
@crodjer crodjer Add myself to authors
Signed-off-by: Rohan Jain <crodjer@gmail.com>
ce27fe1
@crodjer crodjer 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
@crodjer

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.

crodjer added some commits
@crodjer crodjer Extend session key char set
Signed-off-by: Rohan Jain <crodjer@gmail.com>
f5700b9
@crodjer crodjer 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
@crodjer crodjer Remove unused imports
Signed-off-by: Rohan Jain <crodjer@gmail.com>
877edf6
@crodjer crodjer 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
@crodjer crodjer 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
@ptone
Collaborator

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

@crodjer

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

@ptone
Collaborator

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!

@ptone ptone closed this
@crodjer

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
Commits on May 28, 2012
  1. @crodjer

    Add myself to authors

    crodjer authored
    Signed-off-by: Rohan Jain <crodjer@gmail.com>
  2. @crodjer

    Check session expiry on the serve side

    crodjer authored
    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>
Commits on Jun 4, 2012
  1. @crodjer

    Extend session key char set

    crodjer authored
    Signed-off-by: Rohan Jain <crodjer@gmail.com>
  2. @crodjer

    Session cleanup management command improvements

    crodjer authored
    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>
  3. @crodjer

    Remove unused imports

    crodjer authored
    Signed-off-by: Rohan Jain <crodjer@gmail.com>
  4. @crodjer

    Compatibility decoding of existing sessions

    crodjer authored
    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>
  5. @crodjer

    Make compatibility with older mechanism optional

    crodjer authored
    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>
This page is out of date. Refresh to see the latest.
View
1  AUTHORS
@@ -566,6 +566,7 @@ answer newbie questions, and generally made Django that much better:
Gasper Zejn <zejn@kiberpipa.org>
Jarek Zgoda <jarek.zgoda@gmail.com>
Cheng Zhang
+ Rohan Jain <crodjer@gmail.com>
A big THANK YOU goes to:
View
57 django/contrib/sessions/backends/base.py
@@ -1,6 +1,6 @@
import base64
-import time
from datetime import datetime, timedelta
+
try:
import cPickle as pickle
except ImportError:
@@ -8,10 +8,11 @@
from django.conf import settings
from django.core.exceptions import SuspiciousOperation
-from django.utils.crypto import constant_time_compare
from django.utils.crypto import get_random_string
from django.utils.crypto import salted_hmac
+from django.utils.crypto import constant_time_compare
from django.utils import timezone
+from django.core import signing
class CreateError(Exception):
"""
@@ -27,10 +28,15 @@ class SessionBase(object):
TEST_COOKIE_NAME = 'testcookie'
TEST_COOKIE_VALUE = 'worked'
+ # Session_key should not be case sensitive because some backends can store
+ # it on case insensitive file systems.
+ VALID_KEY_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789"
+
def __init__(self, session_key=None):
self._session_key = session_key
self.accessed = False
self.modified = False
+ self.signer = signing.TimestampSigner()
def __contains__(self, key):
return key in self._session
@@ -76,11 +82,43 @@ def _hash(self, value):
def encode(self, session_dict):
"Returns the given session dictionary pickled and encoded as a string."
+ serialized = pickle.dumps(session_dict)
+ return self.signer.sign(serialized)
+
+ def decode(self, session_data):
+ try:
+ try:
+ serialized = str(self.signer.unsign(session_data,
+ settings.SESSION_COOKIE_AGE))
+
+ except signing.SignatureExpired:
+ # An expired signature means it is not a compatibility issue so
+ # raise it.
+ raise
+
+ except signing.BadSignature:
+ if getattr(settings, 'SESSION_KEEP_COMPATIBLE'):
+ return self.decode_legacy(session_data)
+ else:
+ raise
+
+ return pickle.loads(serialized)
+
+ except Exception:
+ # ValueError, SuspiciousOperation, BadSignature, unpickling
+ # exceptions. If any of these happen, just return an empty
+ # dictionary (an empty session).
+ self.modified = True
+ return {}
+
+ def encode_legacy(self, session_dict):
+ "Encoding mechanism used earlier. Deprecated"
pickled = pickle.dumps(session_dict, pickle.HIGHEST_PROTOCOL)
hash = self._hash(pickled)
return base64.encodestring(hash + ":" + pickled)
- def decode(self, session_data):
+ def decode_legacy(self, session_data):
+ "Decoding mechanism used earlier. Deprecated"
encoded_data = base64.decodestring(session_data)
try:
# could produce ValueError if there is no ':'
@@ -130,12 +168,8 @@ def clear(self):
def _get_new_session_key(self):
"Returns session key that isn't being used."
- # Todo: move to 0-9a-z charset in 1.5
- hex_chars = '1234567890abcdef'
- # session_key should not be case sensitive because some backends
- # can store it on case insensitive file systems.
while True:
- session_key = get_random_string(32, hex_chars)
+ session_key = get_random_string(32, self.VALID_KEY_CHARS)
if not self.exists(session_key):
break
return session_key
@@ -278,3 +312,10 @@ def load(self):
Loads the session data and returns a dictionary.
"""
raise NotImplementedError
+
+ @classmethod
+ def cleanup(cls):
+ """
+ Cleaunp the expired sessions
+ """
+ raise NotImplementedError
View
4 django/contrib/sessions/backends/db.py
@@ -72,6 +72,10 @@ def delete(self, session_key=None):
except Session.DoesNotExist:
pass
+ @classmethod
+ def cleanup(cls):
+ Session.objects.filter(expire_date__lt=timezone.now()).delete()
+ transaction.commit_unless_managed()
# At bottom to avoid circular import
from django.contrib.sessions.models import Session
View
19 django/contrib/sessions/backends/file.py
@@ -26,8 +26,6 @@ def __init__(self, session_key=None):
self.file_prefix = settings.SESSION_COOKIE_NAME
super(SessionStore, self).__init__(session_key)
- VALID_KEY_CHARS = set("abcdef0123456789")
-
def _key_to_file(self, session_key=None):
"""
Get the file associated with this session key.
@@ -38,7 +36,7 @@ def _key_to_file(self, session_key=None):
# Make sure we're not vulnerable to directory traversal. Session keys
# should always be md5s, so they should never contain directory
# components.
- if not set(session_key).issubset(self.VALID_KEY_CHARS):
+ if not set(session_key).issubset(set(self.VALID_KEY_CHARS)):
raise SuspiciousOperation(
"Invalid characters in session key")
@@ -142,3 +140,18 @@ def delete(self, session_key=None):
def clean(self):
pass
+
+ @classmethod
+ def cleanup(cls):
+ storage_path = getattr(settings, "SESSION_FILE_PATH", tempfile.gettempdir())
+ file_prefix = settings.SESSION_COOKIE_NAME
+
+ # Get all file sessions stored
+ sessions = [cls(session_file[len(file_prefix):])
+ for session_file in os.listdir(storage_path)
+ if session_file.startswith(file_prefix)]
+
+ # Cleanup all empty sessions
+ for session in sessions:
+ if not session.load():
+ session.delete()
View
37 django/contrib/sessions/tests.py
@@ -1,4 +1,4 @@
-from datetime import datetime, timedelta
+from datetime import timedelta
import shutil
import string
import tempfile
@@ -253,6 +253,26 @@ def test_decode(self):
encoded = self.session.encode(data)
self.assertEqual(self.session.decode(encoded), data)
+ def test_decode_legacy(self):
+ # Ensure we can decode what we encode
+ data = {'a test key': 'a test value'}
+ encoded = self.session.encode_legacy(data)
+ self.assertEqual(self.session.decode_legacy(encoded), data)
+
+ def test_decode_compatibility_disabled(self):
+ # Test that session data encoded with legacy mechanisms is reset when
+ # compatibility is disabled
+ data = {'a test key': 'a test value'}
+ encoded = self.session.encode_legacy(data)
+ self.assertEqual(self.session.decode(encoded), {})
+
+ @override_settings(SESSION_KEEP_COMPATIBLE=True)
+ def test_decode_compatibility_enabled(self):
+ # Test that session data encoded with legacy mechanisms is not reset
+ # when compatibility is enabled
+ data = {'a test key': 'a test value'}
+ encoded = self.session.encode_legacy(data)
+ self.assertEqual(self.session.decode(encoded), data)
class DatabaseSessionTests(SessionTestsMixin, TestCase):
@@ -345,6 +365,21 @@ def test_invalid_key_forwardslash(self):
self.assertRaises(SuspiciousOperation,
self.backend("a/b/c").load)
+ # This test fails with cookie (which is fine I suppose) and cache backends,
+ # thats why added it to file tests only.
+ @override_settings(SESSION_COOKIE_AGE=0)
+ def test_onload_expiry_check(self):
+ """
+ Test to ensure that expiry of session is checked on-load
+ """
+
+ # Setup a test cookie
+ self.session.set_test_cookie()
+ self.assertTrue(self.session.test_cookie_worked())
+
+ self.session.load()
+ # The test data should be absent now, as the cookie age is 0
+ self.assertFalse(self.session.test_cookie_worked())
class CacheSessionTests(SessionTestsMixin, unittest.TestCase):
View
9 django/core/management/commands/cleanup.py
@@ -1,11 +1,10 @@
from django.core.management.base import NoArgsCommand
-from django.utils import timezone
+from django.utils.importlib import import_module
+from django.conf import settings
class Command(NoArgsCommand):
help = "Can be run as a cronjob or directly to clean out old data from the database (only expired sessions at the moment)."
def handle_noargs(self, **options):
- from django.db import transaction
- from django.contrib.sessions.models import Session
- Session.objects.filter(expire_date__lt=timezone.now()).delete()
- transaction.commit_unless_managed()
+ engine = import_module(settings.SESSION_ENGINE)
+ engine.SessionStore.cleanup()
View
17 docs/topics/http/sessions.txt
@@ -568,6 +568,23 @@ Whether to save the session data on every request. If this is ``False``
(default), then the session data will only be saved if it has been modified --
that is, if any of its dictionary values have been assigned or deleted.
+SESSION_KEEP_COMPATIBLE
+-----------------------
+
+Default: ``False``
+
+Wheather to continue support for existing sessions, which were created with
+older mechanism. This is useful if you want the existing sessions not to
+invalidate. If set to True, the legacy sessions will be gradually converted to
+the signed sessions, after that this setting can be set to ``False`` again.
+
+.. warning::
+
+ Setting this to ``True`` will make you susceptible to all the security risks
+ from the previous mechanism which lacked data signing. Use this if you don't
+ want all the users to automatically logout after a django upgrade.
+
+
.. _Django settings: ../settings/
Technical details
Something went wrong with that request. Please try again.