[Zope-Checkins] SVN: Zope/hotfixes/ Import POST-only hotfix
Martijn Pieters
mj at zopatista.com
Tue Mar 20 05:09:04 EDT 2007
Log message for revision 73391:
Import POST-only hotfix
Changed:
A Zope/hotfixes/README.txt
A Zope/hotfixes/__init__.py
A Zope/hotfixes/tests/
A Zope/hotfixes/tests/__init__.py
A Zope/hotfixes/tests/test_hotfix.py
A Zope/hotfixes/version.txt
-=-
Added: Zope/hotfixes/README.txt
===================================================================
--- Zope/hotfixes/README.txt 2007-03-20 09:05:56 UTC (rev 73390)
+++ Zope/hotfixes/README.txt 2007-03-20 09:09:02 UTC (rev 73391)
@@ -0,0 +1,62 @@
+Hotfix-20070320 README
+
+ This hotfix corrects a cross-site scripting vulnerability in Zope2,
+ where an attacker can use a hidden GET request to leverage a
+ authenticated user's credentials to alter security settings and/or
+ user accounts.
+
+ Note that this fix only protects against GET requests, any site that
+ allows endusers to create auto-submitting forms (through javascript)
+ will remain vulnerable.
+
+ The hotfix may be removed after upgrading to a version of Zope2 more
+ recent than this hotfix.
+
+ Affected Versions
+
+ - Zope 2.8.0 - 2.8.8
+
+ - Zope 2.9.0 - 2.9.6
+
+ - Zope 2.10.0 - 2.10.2
+
+ - Earlier versions of Zope 2 are affected as well, but no new
+ releases for older major Zope releases (Zope 2.7 and earlier) will
+ be made. This Hotfix may work for older versions, but this has not
+ been tested.
+
+ Installing the Hotfix
+
+ This hotfix is installed as a standard Zope2 product. The following
+ examples assume that your Zope instance is located at
+ '/var/zope/instance': please adjust according to your actual
+ instance path. Also note that hotfix products are *not* intended
+ for installation into the "software home" of your Zope.
+
+ 1. Unpack the tarball / zipfile for the Hotfix into a temporary
+ location::
+
+ $ cd /tmp
+ $ tar xzf ~/Hotfix_20070320.tar.gz
+
+ 2. Copy or move the product directory from the unpacked directory
+ to the 'Products' directory of your Zope instance::
+
+ $ cp -a /tmp/Hotfix_20070320/ /var/zope/instance/Products/
+
+ 3. Restart Zope::
+
+ $ /var/zope/instance/bin/zopectl restart
+
+ Uninstalling the Hotfix
+
+ After upgrading Zope to one of the fixed versions, you should remove
+ this hotfix product from your Zope instance.
+
+ 1. Remove the product directory from your instance 'Products'::
+
+ $ rm -rf /var/zope/instance/Products/Hotfix_20070320/
+
+ 2. Restart Zope::
+
+ $ /var/zope/instance/bin/zopectl restart
Added: Zope/hotfixes/__init__.py
===================================================================
--- Zope/hotfixes/__init__.py 2007-03-20 09:05:56 UTC (rev 73390)
+++ Zope/hotfixes/__init__.py 2007-03-20 09:09:02 UTC (rev 73391)
@@ -0,0 +1,122 @@
+#############################################################################
+#
+# Copyright (c) 2007 Zope Corporation and Contributors. All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE
+#
+##############################################################################
+
+"""Hotfix_20070319
+
+Protect security methods against GET requests.
+
+"""
+
+import inspect
+from zExceptions import Forbidden
+from ZPublisher.HTTPRequest import HTTPRequest
+
+def _buildFacade(spec, docstring):
+ """Build a facade function, matching the decorated method in signature.
+
+ Note that defaults are replaced by None, and _curried will reconstruct
+ these to preserve mutable defaults.
+
+ """
+ args = inspect.formatargspec(formatvalue=lambda v: '=None', *spec)
+ callargs = inspect.formatargspec(formatvalue=lambda v: '', *spec)
+ return 'def _facade%s:\n """%s"""\n return _curried%s' % (
+ args, docstring, callargs)
+
+def postonly(callable):
+ """Only allow callable when request method is POST."""
+ spec = inspect.getargspec(callable)
+ args, defaults = spec[0], spec[3]
+ try:
+ r_index = args.index('REQUEST')
+ except ValueError:
+ raise ValueError('No REQUEST parameter in callable signature')
+
+ arglen = len(args)
+ if defaults is not None:
+ defaults = zip(args[arglen - len(defaults):], defaults)
+ arglen -= len(defaults)
+
+ def _curried(*args, **kw):
+ request = None
+ if len(args) > r_index:
+ request = args[r_index]
+
+ if isinstance(request, HTTPRequest):
+ if request.get('REQUEST_METHOD', 'GET').upper() != 'POST':
+ raise Forbidden('Request must be POST')
+
+ # Reconstruct keyword arguments
+ if defaults is not None:
+ args, kwparams = args[:arglen], args[arglen:]
+ for positional, (key, default) in zip(kwparams, defaults):
+ if positional is None:
+ kw[key] = default
+ else:
+ kw[key] = positional
+
+ return callable(*args, **kw)
+
+ facade_globs = dict(_curried=_curried)
+ exec _buildFacade(spec, callable.__doc__) in facade_globs
+ return facade_globs['_facade']
+
+# Add REQUEST to BasicUserFolder.userFolder* methods as well as protect them
+from AccessControl.User import BasicUserFolder
+
+_original_ufAddUser = BasicUserFolder.userFolderAddUser
+def ufAddUser(self, name, password, roles, domains, REQUEST=None, **kw):
+ return _original_ufAddUser(self, name, password, roles, domains, **kw)
+ufAddUser.__doc__ = _original_ufAddUser.__doc__
+BasicUserFolder.userFolderAddUser = postonly(ufAddUser)
+
+_original_ufEditUser = BasicUserFolder.userFolderEditUser
+def ufEditUser(self, name, password, roles, domains, REQUEST=None, **kw):
+ return _original_ufEditUser(self, name, password, roles, domains, **kw)
+ufEditUser.__doc__ = _original_ufEditUser.__doc__
+BasicUserFolder.userFolderEditUser = postonly(ufEditUser)
+
+_original_ufDelUsers = BasicUserFolder.userFolderDelUsers
+def ufDelUsers(self, names, REQUEST=None):
+ return _original_ufDelUsers(self, names)
+ufDelUsers.__doc__ = _original_ufDelUsers.__doc__
+BasicUserFolder.userFolderDelUsers = postonly(ufDelUsers)
+
+BasicUserFolder.manage_setUserFolderProperties = postonly(
+ BasicUserFolder.manage_setUserFolderProperties)
+BasicUserFolder._addUser = postonly(BasicUserFolder._addUser)
+BasicUserFolder._changeUser = postonly(BasicUserFolder._changeUser)
+BasicUserFolder._delUsers = postonly(BasicUserFolder._delUsers)
+
+from AccessControl.Owned import Owned
+Owned.manage_takeOwnership = postonly(Owned.manage_takeOwnership)
+Owned.manage_changeOwnershipType = postonly(Owned.manage_changeOwnershipType)
+
+from AccessControl.PermissionMapping import RoleManager as PMRM
+PMRM.manage_setPermissionMapping = postonly(PMRM.manage_setPermissionMapping)
+
+from AccessControl.Role import RoleManager as RMRM
+RMRM.manage_acquiredPermissions = postonly(RMRM.manage_acquiredPermissions)
+RMRM.manage_permission = postonly(RMRM.manage_permission)
+RMRM.manage_changePermissions = postonly(RMRM.manage_changePermissions)
+RMRM.manage_addLocalRoles = postonly(RMRM.manage_addLocalRoles)
+RMRM.manage_setLocalRoles = postonly(RMRM.manage_setLocalRoles)
+RMRM.manage_delLocalRoles = postonly(RMRM.manage_delLocalRoles)
+RMRM._addRole = postonly(RMRM._addRole)
+RMRM._delRoles = postonly(RMRM._delRoles)
+
+from OFS.DTMLMethod import DTMLMethod
+DTMLMethod.manage_proxy = postonly(DTMLMethod.manage_proxy)
+
+from Products.PythonScripts.PythonScript import PythonScript
+PythonScript.manage_proxy = postonly(PythonScript.manage_proxy)
Added: Zope/hotfixes/tests/__init__.py
===================================================================
Added: Zope/hotfixes/tests/test_hotfix.py
===================================================================
--- Zope/hotfixes/tests/test_hotfix.py 2007-03-20 09:05:56 UTC (rev 73390)
+++ Zope/hotfixes/tests/test_hotfix.py 2007-03-20 09:09:02 UTC (rev 73391)
@@ -0,0 +1,141 @@
+import StringIO
+from Testing.ZopeTestCase import FunctionalTestCase, user_name, user_password
+
+class NoGETTest(FunctionalTestCase):
+ def afterSetUp(self):
+ self.folder_path = '/'+self.folder.absolute_url(1)
+ self.setRoles(('Manager',))
+
+ def _onlyPOST(self, path, qstring='', success=200, rpath=None):
+ basic_auth = '%s:%s' % (user_name, user_password)
+ env = dict()
+ if rpath:
+ env['HTTP_REFERER'] = self.app.absolute_url() + rpath
+ response = self.publish('%s?%s' % (path, qstring), basic_auth, env)
+ self.assertEqual(response.getStatus(), 403)
+
+ data = StringIO.StringIO(qstring)
+ response = self.publish(path, basic_auth, env, request_method='POST',
+ stdin=data)
+ self.assertEqual(response.getStatus(), success)
+
+ def test_userFolderAddUser(self):
+ path = self.folder_path + '/acl_users/userFolderAddUser'
+ qstring = 'name=foo&password=bar&domains=&roles:list=Manager'
+ self._onlyPOST(path, qstring)
+
+ def test_userFolderEditUser(self):
+ path = self.folder_path + '/acl_users/userFolderEditUser'
+ qstring = 'name=%s&password=bar&domains=&roles:list=Manager' % (
+ user_name)
+ self._onlyPOST(path, qstring)
+
+ def test_userFolderDelUsers(self):
+ path = self.folder_path + '/acl_users/userFolderDelUsers'
+ qstring = 'names:list=%s' % user_name
+ self._onlyPOST(path, qstring)
+
+ def test_manage_setUserFolderProperties(self):
+ path = self.folder_path + '/acl_users/manage_setUserFolderProperties'
+ qstring = 'encrypt_passwords=1'
+ self._onlyPOST(path, qstring)
+
+ def test_addUser(self):
+ # _addUser is called indirectly
+ path = self.folder_path + '/acl_users/manage_users'
+ qstring = ('submit=Add&name=foo&password=bar&confirm=bar&domains=&'
+ 'roles:list=Manager')
+ self._onlyPOST(path, qstring)
+
+ def test_changeUser(self):
+ # _changeUser is called indirectly
+ path = self.folder_path + '/acl_users/manage_users'
+ qstring = ('submit=Change&name=%s&password=bar&confirm=bar&domains=&'
+ 'roles:list=Manager' % user_name)
+ self._onlyPOST(path, qstring)
+
+ def test_delUser(self):
+ # _delUsers is called indirectly
+ path = self.folder_path + '/acl_users/manage_users'
+ qstring = ('submit=Delete&names:list=%s' % user_name)
+ self._onlyPOST(path, qstring)
+
+ def test_manage_takeOwnership(self):
+ self.setRoles(('Owner',))
+ path = self.folder_path + '/acl_users/manage_takeOwnership'
+ rpath = self.folder_path + '/acl_users/manage_owner'
+ self._onlyPOST(path, success=302, rpath=rpath)
+
+ def test_manage_changeOwnershipType(self):
+ self.setRoles(('Owner',))
+ path = self.folder_path + '/acl_users/manage_changeOwnershipType'
+ self._onlyPOST(path, success=302)
+
+ def test_manage_setPermissionMapping(self):
+ path = self.folder_path + '/manage_setPermissionMapping'
+ qstring = 'permission_names:list=Foo&class_permissions:list=View'
+ self._onlyPOST(path, qstring)
+
+ def test_manage_acquiredPermissions(self):
+ path = self.folder_path + '/manage_acquiredPermissions'
+ qstring = 'permissions:list=View'
+ self._onlyPOST(path, qstring)
+
+ def test_manage_permission(self):
+ path = self.folder_path + '/manage_permission'
+ qstring = 'permission_to_manage=View&roles:list=Manager'
+ self._onlyPOST(path, qstring)
+
+ def test_manage_changePermissions(self):
+ path = self.folder_path + '/manage_changePermissions'
+ self._onlyPOST(path)
+
+ def test_manage_addLocalRoles(self):
+ path = self.folder_path + '/manage_addLocalRoles'
+ qstring = 'userid=Foo&roles:list=Manager'
+ self._onlyPOST(path, qstring)
+
+ def test_manage_setLocalRoles(self):
+ path = self.folder_path + '/manage_setLocalRoles'
+ qstring = 'userid=Foo&roles:list=Manager'
+ self._onlyPOST(path, qstring)
+
+ def test_manage_delLocalRoles(self):
+ path = self.folder_path + '/manage_delLocalRoles'
+ qstring = 'userids:list=Foo'
+ self._onlyPOST(path, qstring)
+
+ def test_addRole(self):
+ # _addRole is called indirectly
+ path = self.folder_path + '/manage_defined_roles'
+ qstring = 'submit=Add+Role&role=Foo'
+ self._onlyPOST(path, qstring)
+
+ def test_delRoles(self):
+ # _delRoles is called indirectly
+ path = self.folder_path + '/manage_defined_roles'
+ qstring = 'submit=Delete+Role&role=Foo'
+ self._onlyPOST(path, qstring)
+
+ def test_DTMLMethod_manage_proxy(self):
+ self.folder.addDTMLMethod('dtmlmethod')
+ path = self.folder_path + '/dtmlmethod/manage_proxy'
+ qstring = 'roles:list=Manager'
+ self._onlyPOST(path, qstring)
+
+ def test_PythonScript_manage_proxy(self):
+ from Testing.ZopeTestCase import installProduct
+ installProduct('PythonScripts')
+ dispatcher = self.folder.manage_addProduct['PythonScripts']
+ dispatcher.manage_addPythonScript('pythonscript')
+ path = self.folder_path + '/pythonscript/manage_proxy'
+ qstring = 'roles:list=Manager'
+ self._onlyPOST(path, qstring)
+
+def test_suite():
+ from unittest import makeSuite
+ return makeSuite(NoGETTest)
+
+if __name__ == '__main__':
+ import unittest
+ unittest.main()
Added: Zope/hotfixes/version.txt
===================================================================
--- Zope/hotfixes/version.txt 2007-03-20 09:05:56 UTC (rev 73390)
+++ Zope/hotfixes/version.txt 2007-03-20 09:09:02 UTC (rev 73391)
@@ -0,0 +1 @@
+Hotfix_20070320
More information about the Zope-Checkins
mailing list