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 browser id (eg. www.yourdomain.org?_ZopeId=39671465A4.bnRkCL8w). Zope will trust that id and use it for further requests. Anyway you should disable the acceptance via forms, query strings and URL.
Assumed some proxy will cache the Set-Cookie header containing a browser id. As a result all visitors will share the same session object!
Let's eliminate this insecure and annoying behaviour.
Open the file Products/Sessions/BrowserIdManager.py and edit the method getBrowserId of class BrowserIdManager:
security.declareProtected(ACCESS_CONTENTS_PERM, 'getBrowserId')
def getBrowserId(self, create=1):
"""
Examines the request and hands back browser id value or
None if no id exists. If there is no browser id
and if 'create' is true, create one. If cookies are are
an allowable id namespace and create is true, set one. Stuff
the id and the namespace it was found in into the REQUEST object
for further reference during this request.
"""
REQUEST = self.REQUEST
# let's see if bid has already been attached to request
bid = getattr(REQUEST, 'browser_id_', None)
if bid is not None:
# it's already set in this request so we can just return it
# if it's well-formed
- if not isAWellFormedBrowserId(bid):
+ if not isAWellFormedBrowserId(bid, bidm=self):
# somebody screwed with the REQUEST instance during
# this request.
raise BrowserIdManagerErr, (
'Ill-formed browserid in REQUEST.browser_id_: %s' %
escape(bid)
)
return bid
# fall through & ck form/cookie namespaces if bid is not in request.
tk = self.browserid_name
ns = self.browserid_namespaces
for name in ns:
if name == 'url':
continue # browser ids in url are checked by Traverser class
current_ns = getattr(REQUEST, name, None)
if current_ns is None:
continue
bid = current_ns.get(tk, None)
if bid is not None:
# hey, we got a browser id!
- if isAWellFormedBrowserId(bid):
+ if isAWellFormedBrowserId(bid, bidm=self):
# bid is not "plain old broken"
REQUEST.browser_id_ = bid
REQUEST.browser_id_ns_ = name
return bid
# fall through if bid is invalid or not in namespaces
if create:
# create a brand new bid
- bid = getNewBrowserId()
- if 'cookies' in ns:
- self._setCookie(bid, REQUEST)
- REQUEST.browser_id_ = bid
- REQUEST.browser_id_ns_ = None
- return bid
+ return self.newBrowserId()
# implies a return of None if:
# (not create=1) and (invalid or ((not in req) and (not in ns)))
Also add the new methods newBrowserId and _getKey to class BrowserIdManager:
+ security.declareProtected(ACCESS_CONTENTS_PERM, 'newBrowserId')
+ def newBrowserId(self):
+ """ Set new browser id and update reference to an 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 could be found.')
+ if session_data_manager.hasSessionData():
+ session = session_data_manager.getSessionData()
+ else:
+ session = None
+ # create a brand new bid
+ bid = getNewBrowserId(bidm=self)
+ 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
+ def _getKey(self, new=False):
+ if new or not hasattr(self, '_key'):
+ self._key = ''.join([random.choice(string.ascii_letters+string.digits) for i in range(8)])
+ # Uncomment the following line if you want to include the HTTP_USER_AGENT.
+ #return '%s%08x' % (self._key, binascii.crc32(self.REQUEST.get('HTTP_USER_AGENT', '')) & 0xffffffff)
+ return self._key
Modify the class BrowserIdManagerTraverser:
class BrowserIdManagerTraverser(Persistent):
def __call__(self, container, request, browser_id=None,
browser_id_ns=None,
BROWSERID_MANAGER_NAME=BROWSERID_MANAGER_NAME):
"""
Registered hook to set and get a browser id in the URL. If
a browser id is found in the URL of an incoming request, we put it
into a place where it can be found later by the browser id manager.
If our browser id manager's auto-url-encoding feature is on, cause
Zope-generated URLs to contain the browser id by rewriting the
request._script list.
"""
browser_id_manager = getattr(container, BROWSERID_MANAGER_NAME, None)
# fail if we cannot find a browser id manager (that means this
# instance has been "orphaned" somehow)
if browser_id_manager is None:
LOG.error('Could not locate browser id manager!')
return
try:
stack = request['TraversalRequestNameStack']
request.browser_id_ns_ = browser_id_ns
bid_name = browser_id_manager.getBrowserIdName()
# stuff the browser id and id namespace into the request
# if the URL has a browser id name and browser id as its first
# two elements. Only remove these elements from the
# traversal stack if they are a "well-formed pair".
if len(stack) >= 2 and stack[-1] == bid_name:
- if isAWellFormedBrowserId(stack[-2]):
+ if isAWellFormedBrowserId(stack[-2], bidm=browser_id_manager):
name = stack.pop() # pop the name off the stack
browser_id = stack.pop() # pop id off the stack
request.browser_id_ = browser_id
request.browser_id_ns_ = 'url'
# if the browser id manager is set up with 'auto url encoding',
# cause generated URLs to be encoded with the browser id name/value
# pair by munging request._script.
if browser_id_manager.getAutoUrlEncoding():
if browser_id is None:
- request.browser_id_ = browser_id = getNewBrowserId()
+ request.browser_id_ = browser_id = getNewBrowserId(bidm=browser_id_manager)
request._script.append(quote(bid_name))
request._script.append(quote(browser_id))
except:
LOG.error('indeterminate error', exc_info=sys.exc_info())
Then modify the functions getBrowserIdPieces, isAWellFormedBrowserId and getNewBrowserId:
def getBrowserIdPieces(bid):
""" returns browser id parts in a tuple consisting of rand_id,
timestamp
"""
- return (bid[:8], bid[8:19])
+ return (bid[:8], bid[8:19], bid[19:27])
-def isAWellFormedBrowserId(bid, binerr=binascii.Error):
+def isAWellFormedBrowserId(bid, binerr=binascii.Error, bidm=None):
+ if bidm:
+ key = bidm._getKey()
+ lifetime = bidm.getCookieLifeDays() or 1
+ else:
+ key = ''
+ lifetime = 1
try:
- rnd, ts = getBrowserIdPieces(bid)
+ rnd, ts, crc = getBrowserIdPieces(bid)
+ if crc != '%08x' % (binascii.crc32(rnd+ts+key) & 0xffffffff):
+ raise ValueError, 'invalid crc'
int(rnd)
- getB64TStampToInt(ts)
+ ts = getB64TStampToInt(ts)
+ now = time.time()
+ if ts > now + 5 or ts + lifetime * 86400 < now:
+ raise ValueError, 'bid expired'
return bid
except (TypeError, ValueError, AttributeError, IndexError, binerr):
return None
-def getNewBrowserId(randint=random.randint, maxint=99999999):
+def getNewBrowserId(randint=random.randint, maxint=99999999, bidm=None):
""" Returns 19-character string browser id
'AAAAAAAABBBBBBBB'
where:
A == leading-0-padded 8-char string-rep'd random integer
B == modified base64-encoded 11-char timestamp
To be URL-compatible, base64 encoding is modified as follows:
'=' end-padding is stripped off
'+' is translated to '-'
'/' is translated to '.'
An example is: 89972317A0C3EHnUi90w
"""
+ if bidm:
+ key = bidm._getKey()
+ else:
+ key = ''
- return '%08i%s' % (randint(0, maxint-1), getB64TStamp())
+ bid = '%08i%s' % (randint(0, maxint-1), getB64TStamp())
+ return '%s%08x' % (bid, binascii.crc32(bid+key) & 0xffffffff)
Now each BrowserIdManager has a random key and the generated browser ids will be protected by a crc hash. Furthermore a browser id will be invalidated server-sided if it has been expired. If you have a cookie lifetime of 0 (lifetime of browser) it 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 browser id after login to prevent a session fixation attack.