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 _______________________________________________ Zope-Checkins maillist - Zope-Checkins@zope.org http://mail.zope.org/mailman/listinfo/zope-checkins