joonis Logo

Unreliable Session Management in Zope

Zope comes with a built-in Session Management. The Browser Id Manager identifies a particular browser based on a unique browser id which is then referenced to a session object if needed.

But that browser id can be manipulated what makes session fixation attacks easy.

Assumed someone links to your website with a generally valid session id (eg. www.yourdomain.org?_ZopeId=39671465A4.bnRkCL8w). Zope will trust this id and uses it for further requests. Anyway you should disable the acceptance via forms, query strings and URL.

With the following patch of the BrowserIdManager you eliminate this insecure and annoying behaviour. Just create a new folder under your Product directory and save the file as __init__.py.

After restarting Zope each BrowserIdManager has a random key and the generated session ids are protected by a HMAC hash. Furthermore a session id will be invalidated server-sided if it has been expired. If you have a cookie lifetime of 0 (lifetime of browser), the session id will be invalidated after one day at the latest.

There is also the possibility to regenerate the browser id via REQUEST.SESSION.getBrowserIdManager().newBrowserId(). If you use some authentication method based on sessions (eg. SessionCrumbler) you can create a new session id after login to prevent a session fixation attack.

bidm_patch.py [4.27 KB]
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
# -*- coding: utf-8 -*
#
#  Patch of Zope's BrowserIdManager
#

import sys
import time
import uuid
import hmac
from hashlib import sha256
from base64 import b64encode, b64decode
from Products.Sessions import BrowserIdManager
from Products.Sessions.interfaces import BrowserIdManagerErr
from Products.Sessions.SessionPermissions import ACCESS_CONTENTS_PERM
from AccessControl.class_init import InitializeClass
from AccessControl.SecurityInfo import ClassSecurityInfo


BrowserIdManager.BrowserIdManager.security = ClassSecurityInfo()
BrowserIdManager.BrowserIdManager.security.declareProtected(
                            ACCESS_CONTENTS_PERM, 'newBrowserId')
def newBrowserId(self):
    """Set new browser id and update reference to existing session object."""
    REQUEST = self.REQUEST
    # try to get session object
    session_data_manager = getattr(self, 'session_data_manager', None)
    if session_data_manager is None:
        raise BrowserIdManagerErr('no session_data_manager found')
    if session_data_manager.hasSessionData():
        session = session_data_manager.getSessionData()
    else:
        session = None
    # create a brand new bid
    bid = getNewBrowserId()
    if 'cookies' in self.browserid_namespaces:
        self._setCookie(bid, REQUEST)
    REQUEST.browser_id_ = bid
    REQUEST.browser_id_ns_ = None
    # update session reference
    if session:
        container = session_data_manager._getSessionDataContainer()
        current_ts, item = container.__delitem__(session.token)
        session.token = bid
        container[session.token] = item
        # alternatively we could carry existing session data into a new session
        # but this could cause some trouble with existing session references
        #~ session.invalidate()
        #~ new_session = session_data_manager.getSessionData()
        #~ for key, value in session.items():
            #~ new_session.set(key, value)
        #~ #REQUEST.SESSION = new_session # Does that make sense?
    return bid

BrowserIdManager.BrowserIdManager.newBrowserId = newBrowserId


def _get_key(self, new=False):
    if new or not hasattr(self, '_uuid'):
        self._uuid = uuid.uuid4()
    name = ''
    if 'check_ip_address' in self.title:
        name += self.REQUEST.getClientAddr().rsplit('.', 1)[0]
    if 'check_user_agent' in self.title:
        name += self.REQUEST.environ.get('HTTP_USER_AGENT', '')
    return uuid.uuid3(self._uuid, name).bytes

BrowserIdManager.BrowserIdManager._get_key = _get_key

InitializeClass(BrowserIdManager.BrowserIdManager)


def is_equal(val1, val2):
    # constant time comparison
    if len(val1) != len(val2):
        return False
    diff = 0
    for a, b in zip(val1, val2):
        diff += int(a != b)
    return diff == 0


def isAWellFormedBrowserId(bid, binerr=None):
    # ugly fix to get the BrowserIdManager from caller namespace
    f_locals = sys._getframe(1).f_locals
    bidm = f_locals.get('browser_id_manager') or f_locals.get('self')
    if not isinstance(bidm, BrowserIdManager.BrowserIdManager):
        raise BrowserIdManagerErr('could not find BrowserIdManager')
    key = bidm._get_key()
    now = time.time()
    lifetime = (bidm.cookie_life_days or 1) * 86400
    try:
        bid_raw = b64decode(bid, '-.')
        hash, uid = bid_raw[:32], bid_raw[32:]
        # check the hash
        if not is_equal(hash, hmac.new(key, uid, sha256).digest()):
            raise ValueError('invalid hash')
        # check the time
        ts = uuid.UUID(bytes=uid).time / 1e7 - 0x2d8539c80
        if ts > now + 5 or ts + lifetime < now:
            raise ValueError('bid expired')
        return bid
    except:
        return None

BrowserIdManager.isAWellFormedBrowserId = isAWellFormedBrowserId


def getNewBrowserId(randint=None, maxint=None):
    # ugly fix to get the BrowserIdManager from caller namespace
    f_locals = sys._getframe(1).f_locals
    bidm = f_locals.get('browser_id_manager') or f_locals.get('self')
    if not isinstance(bidm, BrowserIdManager.BrowserIdManager):
        raise BrowserIdManagerErr('could not find BrowserIdManager')
    # create a uuid1 that holds a timestamp
    uid = uuid.uuid1(uuid._random_getnode()).bytes
    # protect it with a hash
    hash = hmac.new(bidm._get_key(), uid, sha256).digest()
    return b64encode(hash + uid, '-.')

BrowserIdManager.getNewBrowserId = getNewBrowserId

Can I
  help you?


Just drop me a line at
giraffe@joonis.de