High severityNVD Advisory· Published Sep 30, 2014· Updated May 6, 2026
CVE-2012-5488
CVE-2012-5488
Description
python_scripts.py in Plone before 4.2.3 and 4.3 before beta 1 allows remote attackers to execute Python code via a crafted URL, related to createObject.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
PlonePyPI | < 4.2.3 | 4.2.3 |
PlonePyPI | >= 4.3a0, < 4.3b1 | 4.3b1 |
Affected products
72cpe:2.3:a:plone:plone:*:*:*:*:*:*:*:*+ 71 more
- cpe:2.3:a:plone:plone:*:*:*:*:*:*:*:*range: <=4.2.2
- cpe:2.3:a:plone:plone:1.0:*:*:*:*:*:*:*
- cpe:2.3:a:plone:plone:1.0.1:*:*:*:*:*:*:*
- cpe:2.3:a:plone:plone:1.0.2:*:*:*:*:*:*:*
- cpe:2.3:a:plone:plone:1.0.3:*:*:*:*:*:*:*
- cpe:2.3:a:plone:plone:1.0.4:*:*:*:*:*:*:*
- cpe:2.3:a:plone:plone:1.0.5:*:*:*:*:*:*:*
- cpe:2.3:a:plone:plone:1.0.6:*:*:*:*:*:*:*
- cpe:2.3:a:plone:plone:2.0:*:*:*:*:*:*:*
- cpe:2.3:a:plone:plone:2.0.1:*:*:*:*:*:*:*
- cpe:2.3:a:plone:plone:2.0.2:*:*:*:*:*:*:*
- cpe:2.3:a:plone:plone:2.0.3:*:*:*:*:*:*:*
- cpe:2.3:a:plone:plone:2.0.4:*:*:*:*:*:*:*
- cpe:2.3:a:plone:plone:2.0.5:*:*:*:*:*:*:*
- cpe:2.3:a:plone:plone:2.1:*:*:*:*:*:*:*
- cpe:2.3:a:plone:plone:2.1.1:*:*:*:*:*:*:*
- cpe:2.3:a:plone:plone:2.1.2:*:*:*:*:*:*:*
- cpe:2.3:a:plone:plone:2.1.3:*:*:*:*:*:*:*
- cpe:2.3:a:plone:plone:2.1.4:*:*:*:*:*:*:*
- cpe:2.3:a:plone:plone:2.5:*:*:*:*:*:*:*
- cpe:2.3:a:plone:plone:2.5.1:*:*:*:*:*:*:*
- cpe:2.3:a:plone:plone:2.5.2:*:*:*:*:*:*:*
- cpe:2.3:a:plone:plone:2.5.3:*:*:*:*:*:*:*
- cpe:2.3:a:plone:plone:2.5.4:*:*:*:*:*:*:*
- cpe:2.3:a:plone:plone:2.5.5:*:*:*:*:*:*:*
- cpe:2.3:a:plone:plone:3.0:*:*:*:*:*:*:*
- cpe:2.3:a:plone:plone:3.0.1:*:*:*:*:*:*:*
- cpe:2.3:a:plone:plone:3.0.2:*:*:*:*:*:*:*
- cpe:2.3:a:plone:plone:3.0.3:*:*:*:*:*:*:*
- cpe:2.3:a:plone:plone:3.0.4:*:*:*:*:*:*:*
- cpe:2.3:a:plone:plone:3.0.5:*:*:*:*:*:*:*
- cpe:2.3:a:plone:plone:3.0.6:*:*:*:*:*:*:*
- cpe:2.3:a:plone:plone:3.1:*:*:*:*:*:*:*
- cpe:2.3:a:plone:plone:3.1.1:*:*:*:*:*:*:*
- cpe:2.3:a:plone:plone:3.1.2:*:*:*:*:*:*:*
- cpe:2.3:a:plone:plone:3.1.3:*:*:*:*:*:*:*
- cpe:2.3:a:plone:plone:3.1.4:*:*:*:*:*:*:*
- cpe:2.3:a:plone:plone:3.1.5.1:*:*:*:*:*:*:*
- cpe:2.3:a:plone:plone:3.1.6:*:*:*:*:*:*:*
- cpe:2.3:a:plone:plone:3.1.7:*:*:*:*:*:*:*
- cpe:2.3:a:plone:plone:3.2:*:*:*:*:*:*:*
- cpe:2.3:a:plone:plone:3.2.1:*:*:*:*:*:*:*
- cpe:2.3:a:plone:plone:3.2.2:*:*:*:*:*:*:*
- cpe:2.3:a:plone:plone:3.2.3:*:*:*:*:*:*:*
- cpe:2.3:a:plone:plone:3.3:*:*:*:*:*:*:*
- cpe:2.3:a:plone:plone:3.3.1:*:*:*:*:*:*:*
- cpe:2.3:a:plone:plone:3.3.2:*:*:*:*:*:*:*
- cpe:2.3:a:plone:plone:3.3.3:*:*:*:*:*:*:*
- cpe:2.3:a:plone:plone:3.3.4:*:*:*:*:*:*:*
- cpe:2.3:a:plone:plone:3.3.5:*:*:*:*:*:*:*
- cpe:2.3:a:plone:plone:4.0:*:*:*:*:*:*:*
- cpe:2.3:a:plone:plone:4.0.1:*:*:*:*:*:*:*
- cpe:2.3:a:plone:plone:4.0.2:*:*:*:*:*:*:*
- cpe:2.3:a:plone:plone:4.0.3:*:*:*:*:*:*:*
- cpe:2.3:a:plone:plone:4.0.4:*:*:*:*:*:*:*
- cpe:2.3:a:plone:plone:4.0.5:*:*:*:*:*:*:*
- cpe:2.3:a:plone:plone:4.0.6.1:*:*:*:*:*:*:*
- cpe:2.3:a:plone:plone:4.1:*:*:*:*:*:*:*
- cpe:2.3:a:plone:plone:4.1.4:*:*:*:*:*:*:*
- cpe:2.3:a:plone:plone:4.1.5:*:*:*:*:*:*:*
- cpe:2.3:a:plone:plone:4.1.6:*:*:*:*:*:*:*
- cpe:2.3:a:plone:plone:4.2:*:*:*:*:*:*:*
- cpe:2.3:a:plone:plone:4.2.0.1:*:*:*:*:*:*:*
- cpe:2.3:a:plone:plone:4.2.1:*:*:*:*:*:*:*
- cpe:2.3:a:plone:plone:4.2.1.1:*:*:*:*:*:*:*
- cpe:2.3:a:plone:plone:4.2:a1:*:*:*:*:*:*
- cpe:2.3:a:plone:plone:4.2:a2:*:*:*:*:*:*
- cpe:2.3:a:plone:plone:4.2:b1:*:*:*:*:*:*
- cpe:2.3:a:plone:plone:4.2:b2:*:*:*:*:*:*
- cpe:2.3:a:plone:plone:4.2:rc1:*:*:*:*:*:*
- cpe:2.3:a:plone:plone:4.2:rc2:*:*:*:*:*:*
- cpe:2.3:a:plone:plone:4.3:*:*:*:*:*:*:*
Patches
2c3a98f4e6cf2various security fixes based on PloneHotfix20121106
11 files changed · +293 −3
docs/CHANGES.txt+3 −0 modified@@ -9,6 +9,9 @@ Changelog 4.2.3 (unreleased) ----------------- +- Add various security fixes based on PloneHotfix20121106. + [davisagli] + - Pass minute_step to date_components_support_view.result(). See https://dev.plone.org/ticket/11251 [gbastien]
Products/CMFPlone/patches/__init__.py+2 −0 modified@@ -20,5 +20,7 @@ import iso8601 # use `DateTime.ISO8601` for `DateTime.ISO` iso8601.applyPatches() +import security # misc security fixes + import sendmail sendmail.applyPatches()
Products/CMFPlone/patches/security.py+42 −0 added@@ -0,0 +1,42 @@ +# 1. make sure allow_module can't be called from restricted code +import AccessControl +AccessControl.allow_module.__roles__ = () + +# 2. make sure /@@ doesn't traverse to annotations +from zope.traversing import namespace +from zope.traversing.interfaces import TraversalError +old_traverse = namespace.view.traverse +def traverse(self, name, ignored): + if not name: + raise TraversalError(self.context, name) + return old_traverse(self, name, ignored) +namespace.view.traverse = traverse + +# 3. be sure to check Access contents information permission for FTP users +from AccessControl import getSecurityManager +from zExceptions import Unauthorized +from OFS.ObjectManager import ObjectManager +ObjectManager.__old_manage_FTPlist = ObjectManager.manage_FTPlist +def manage_FTPlist(self, REQUEST): + """Returns a directory listing consisting of a tuple of + (id,stat) tuples, marshaled to a string. Note, the listing it + should include '..' if there is a Folder above the current + one. + + In the case of non-foldoid objects it should return a single + tuple (id,stat) representing itself.""" + + if not getSecurityManager().checkPermission('Access contents information', self): + raise Unauthorized('Not allowed to access contents.') + + return self.__old_manage_FTPlist(REQUEST) +ObjectManager.manage_FTPlist = manage_FTPlist + +# 4. Make sure z3c.form widgets don't get declared as public +from Products.Five.metaconfigure import ClassDirective +old_require = ClassDirective.require +def require(self, *args, **kw): + if self._ClassDirective__class.__module__.startswith('z3c.form.browser'): + return + return old_require(self, *args, **kw) +ClassDirective.require = require
Products/CMFPlone/PloneControlPanel.py+1 −0 modified@@ -266,6 +266,7 @@ def addAction(self, return self.manage_editActionsForm( REQUEST, manage_tabs_message='Added.') + security.declareProtected(ManagePortal, 'registerConfiglet') registerConfiglet = addAction security.declareProtected(ManagePortal, 'manage_editActionsForm')
Products/CMFPlone/PloneTool.py+2 −0 modified@@ -11,6 +11,7 @@ from zope.lifecycleevent import ObjectModifiedEvent from AccessControl import ClassSecurityInfo, Unauthorized +from AccessControl import getSecurityManager from Acquisition import aq_base from Acquisition import aq_inner from Acquisition import aq_parent @@ -1301,6 +1302,7 @@ def renameObjectsByPaths(self, paths, new_ids, new_titles, change_title = new_title and title != new_title changed = False if change_title: + getSecurityManager().validate(obj, obj, 'setTitle', obj.setTitle) obj.setTitle(new_title) notify(ObjectModifiedEvent(obj)) changed = True
Products/CMFPlone/skins/plone_scripts/createObject.cpy+3 −1 modified@@ -19,7 +19,9 @@ response.setHeader('Expires', 'Sat, 01 Jan 2000 00:00:00 GMT') response.setHeader('Cache-Control', 'no-cache') if id is None: - id=context.generateUniqueId(type_name) + id = context.generateUniqueId(type_name) +else: + id = id.replace('$', '$$') if type_name is None: raise Exception, 'Type name not specified'
Products/CMFPlone/skins/plone_scripts/formatColumns.py+4 −0 modified@@ -3,6 +3,10 @@ # returns a list of lists of items +from zExceptions import Forbidden +if container.REQUEST.get('PUBLISHED') is script: + raise Forbidden('Script may not be published.') + rows = [] i = 0
Products/CMFPlone/skins/plone_scripts/getFolderContents.py+4 −0 modified@@ -11,6 +11,10 @@ # NOTE: This script is obsolete, use the browser view # @@folderListing in plone.app.contentlisting +from zExceptions import Forbidden +if container.REQUEST.get('PUBLISHED') is script: + raise Forbidden('Script may not be published.') + mtool = context.portal_membership cur_path = '/'.join(context.getPhysicalPath()) path = {}
Products/CMFPlone/skins/plone_scripts/translate.py+6 −1 modified@@ -6,7 +6,12 @@ ##bind subpath=traverse_subpath ##parameters=msgid, mapping={}, default=None, domain='plone', target_language=None, escape_for_js=False -# handle the possible "nothing" condition in folder_contents.pt ln 21 gracefully +from zExceptions import Forbidden +if container.REQUEST.get('PUBLISHED') is script: + raise Forbidden('Script may not be published.') + +# handle the possible "nothing" condition in folder_contents.pt ln 21 +# gracefully if msgid == None: return None
Products/CMFPlone/skins/plone_scripts/utranslate.py+6 −1 modified@@ -6,7 +6,12 @@ ##bind subpath=traverse_subpath ##parameters=msgid, mapping={}, default=None, domain='plone', target_language=None, escape_for_js=False -# handle the possible "nothing" condition in folder_contents.pt ln 21 gracefully +from zExceptions import Forbidden +if container.REQUEST.get('PUBLISHED') is script: + raise Forbidden('Script may not be published.') + +# handle the possible "nothing" condition in folder_contents.pt ln 21 +# gracefully if msgid == None: return None
Products/CMFPlone/tests/testSecurity.py+220 −0 added@@ -0,0 +1,220 @@ +import re +import unittest +from urllib import urlencode +from Testing.makerequest import makerequest +from Products.PloneTestCase import PloneTestCase as ptc +from Products.Five.testbrowser import Browser +from zExceptions import Unauthorized + +ptc.setupPloneSite() + + +class TestAttackVectorsUnit(unittest.TestCase): + + def test_gtbn_funcglobals(self): + from Products.CMFPlone.utils import getToolByName + try: + getToolByName(self.assertTrue,'func_globals')['__builtins__'] + except TypeError: + pass + else: + self.fail('getToolByName should block access to non CMF tools') + + def test_setHeader_drops_LF(self): + from ZPublisher.HTTPResponse import HTTPResponse + response = HTTPResponse() + response.setHeader('Location', + 'http://www.ietf.org/rfc/\nrfc2616.txt') + self.assertEqual(response.headers['location'], + 'http://www.ietf.org/rfc/rfc2616.txt') + + def test_PT_allow_module_not_available_in_RestrictedPython_1(self): + src = ''' +from AccessControl import Unauthorized +try: + import Products.PlacelessTranslationService +except (ImportError, Unauthorized): + raise AssertionError("Failed to import Products.PTS") +Products.PlacelessTranslationService.allow_module('os') +''' + from Products.PythonScripts.PythonScript import PythonScript + script = makerequest(PythonScript('script')) + script._filepath = 'script' + script.write(src) + self.assertRaises((ImportError, Unauthorized), script) + + def test_PT_allow_module_not_available_in_RestrictedPython_2(self): + src = ''' +from Products.PlacelessTranslationService import allow_module +allow_module('os') +''' + from Products.PythonScripts.PythonScript import PythonScript + script = makerequest(PythonScript('script')) + script._filepath = 'script' + script.write(src) + self.assertRaises((ImportError, Unauthorized), script) + + def test_get_request_var_or_attr_disallowed(self): + import App.Undo + self.assertFalse(hasattr(App.Undo.UndoSupport, 'get_request_var_or_attr')) + + +class TestAttackVectorsFunctional(ptc.FunctionalTestCase): + + def test_widget_traversal_1(self): + res = self.publish('/plone/@@discussion-settings/++widget++moderator_email') + self.assertEqual(302, res.status) + self.assertTrue(res.headers['location'].startswith('http://nohost/plone/acl_users/credentials_cookie_auth/require_login')) + + def test_widget_traversal_2(self): + res = self.publish('/plone/@@discussion-settings/++widget++captcha/terms/field/interface/setTaggedValue?tag=cake&value=lovely') + self.assertEqual(302, res.status) + self.assertTrue(res.headers['location'].startswith('http://nohost/plone/acl_users/credentials_cookie_auth/require_login')) + + def test_registerConfiglet_1(self): + VECTOR = "/plone/portal_controlpanel/registerConfiglet?id=cake&name=Cakey&action=woo&permission=View&icon_expr=" + res = self.publish(VECTOR) + self.assertTrue(res.headers['location'].startswith('http://nohost/plone/acl_users/credentials_cookie_auth/require_login')) + + def test_registerConfiglet_2(self): + VECTOR = "/plone/portal_controlpanel/registerConfiglet?id=cake&name=Cakey&action=woo&permission=View&icon_expr=" + self.publish(VECTOR) + action_ids = [action.id for action in self.portal.portal_controlpanel._actions] + self.assertTrue('cake' not in action_ids) + + def _get_authenticator(self, basic=None): + url = '/plone/login_password' + res = self.publish(url, basic=basic) + m = re.search('name="_authenticator" value="([^"]*)"', res.body) + if m: + return m.group(1) + return '' + + def test_renameObjectsByPaths(self): + PAYLOAD = { + 'paths:list': '/plone/news', + # id must stay the same + 'new_ids:list': 'news', + 'new_titles:list': 'EVIL', + # Set orig_template to 'view'. Otherwise folder_rename "success" redirects + # to folder_contents, which will raise Unauthorized. + 'orig_template': 'view', + } + + browser = Browser() + csrf_token = self._get_authenticator() + + PAYLOAD['_authenticator'] = csrf_token + # Call folder_rename anywhere + browser.open('http://nohost/plone/folder_rename', + urlencode(PAYLOAD)) + self.assertTrue('The following item(s) could not be renamed: /plone/news.' in browser.contents) + self.assertEqual('News', self.portal.news.Title()) + + def test_renameObjectByPaths_postonly(self): + from Products.PythonScripts.PythonScript import PythonScript + script = PythonScript('script') + script._filepath = 'script' + src = """context.plone_utils.renameObjectsByPaths(paths=['/plone/news'], new_ids=['news'], new_titles=['EVIL'], REQUEST=context.REQUEST)""" + script.write(src) + self.portal.evil = script + csrf_token = self._get_authenticator() + + self.publish('/plone/evil', extra={'_authenticator': csrf_token}, request_method='POST') + self.assertEqual('News', self.portal.news.Title()) + + owner_basic = ptc.portal_owner + ':' + ptc.default_password + csrf_token = self._get_authenticator(owner_basic) + self.publish('/plone/evil', extra={'_authenticator': csrf_token}, basic=owner_basic) + self.assertEqual('News', self.portal.news.Title()) + self.publish('/plone/evil', request_method='POST', extra={'_authenticator': csrf_token}, basic=owner_basic) + self.assertEqual('EVIL', self.portal.news.Title()) + + self.setRoles(['Manager']) + self.portal.news.setTitle('News') + self.portal.plone_utils.renameObjectsByPaths(paths=['/plone/news'], new_ids=['news'], new_titles=['EVIL']) + self.assertEqual('EVIL', self.portal.news.Title()) + self.portal.news.setTitle('News') + + self.setRoles(['Member']) + self.portal.plone_utils.renameObjectsByPaths(paths=['/plone/news'], new_ids=['news'], new_titles=['EVIL']) + self.assertEqual('News', self.portal.news.Title()) + + def test_gtbn_faux_archetypes_tool(self): + from Products.CMFPlone.FactoryTool import FauxArchetypeTool + from Products.CMFPlone.utils import getToolByName + self.portal.portal_factory.archetype_tool = FauxArchetypeTool(self.portal.archetype_tool) + self.assertEqual(self.portal.portal_factory.archetype_tool, getToolByName(self.portal.portal_factory, 'archetype_tool')) + + def test_searchForMembers(self): + res = self.publish('/plone/portal_membership/searchForMembers') + self.assertEqual(302, res.status) + self.assertTrue(res.headers['location'].startswith('http://nohost/plone/acl_users/credentials_cookie_auth/require_login')) + + def test_getMemberInfo(self): + res = self.publish('/plone/portal_membership/getMemberInfo?id=admin') + self.assertEqual(404, res.status) + + def test_queryCatalog(self): + res = self.publish('/plone/news/aggregator/queryCatalog') + self.assertEqual(404, res.status) + + def test_resolve_url(self): + res = self.publish("/plone/uid_catalog/resolve_url?path=/evil") + self.assertEqual(302, res.status) + self.assertTrue(res.headers['location'].startswith('http://nohost/plone/acl_users/credentials_cookie_auth/require_login')) + + def test_at_download(self): + self.setRoles(['Manager']) + self.portal.portal_workflow.setChainForPortalTypes(['File'], 'plone_workflow') + self.portal.invokeFactory('File', 'test') + self.portal.portal_workflow.doActionFor(self.portal.test, 'publish') + + # give it a more restricted read_permission + self.portal.test.Schema()['file'].read_permission = 'Manage portal' + + # make sure at_download disallows even though the user has View permission + res = self.publish('/plone/test/at_download/file') + self.assertEqual(res.status, 302) + self.assertTrue(res.headers['location'].startswith('http://nohost/plone/acl_users/credentials_cookie_auth/require_login')) + + def test_ftp(self): + self.setRoles(['Manager', 'Owner']) + self.portal.REQUEST.PARENTS = [self.app] + res = self.portal.news.manage_FTPlist(self.portal.REQUEST) + self.assertTrue(isinstance(res, basestring)) + self.portal.portal_workflow.doActionFor(self.portal.news, 'hide') + self.setRoles(['Member']) + from zExceptions import Unauthorized + self.assertRaises(Unauthorized, self.portal.news.manage_FTPlist, self.portal.REQUEST) + + def test_atat_does_not_return_anything(self): + res = self.publish('/plone/@@') + self.assertEqual(404, res.status) + + def test_go_back(self): + res = self.publish('/plone/front-page/go_back?last_referer=http://${request}', + basic=ptc.portal_owner + ':' + ptc.default_password) + self.assertEqual(302, res.status) + self.assertEqual('http://${request}', res.headers['location'][:17]) + + def test_getFolderContents(self): + res = self.publish('/plone/getFolderContents') + self.assertEqual(403, res.status) + + def test_translate(self): + res = self.publish('/plone/translate?msgid=foo') + self.assertEqual(403, res.status) + + def test_utranslate(self): + res = self.publish('/plone/utranslate?msgid=foo') + self.assertEqual(403, res.status) + + def test_createObject(self): + res = self.publish('/plone/createObject?type_name=File&id=${foo}') + self.assertEqual(302, res.status) + self.assertEqual('http://nohost/plone/portal_factory/File/${foo}/edit', res.headers['location']) + + def test_formatColumns(self): + res = self.publish('/plone/formatColumns?items:list=') + self.assertEqual(403, res.status)
a9479a5b3864various security fixes based on PloneHotfix20121106
11 files changed · +288 −0
docs/CHANGES.txt+3 −0 modified@@ -9,6 +9,9 @@ Changelog 4.3a3 (unreleased) ------------------ +- Add various security fixes based on PloneHotfix20121106. + [davisagli] + - Fix RegistrationTool testPasswordValidity method. See https://dev.plone.org/ticket/13325 [vipod]
Products/CMFPlone/patches/__init__.py+2 −0 modified@@ -20,5 +20,7 @@ import iso8601 # use `DateTime.ISO8601` for `DateTime.ISO` iso8601.applyPatches() +import security # misc security fixes + import sendmail sendmail.applyPatches()
Products/CMFPlone/patches/security.py+42 −0 added@@ -0,0 +1,42 @@ +# 1. make sure allow_module can't be called from restricted code +import AccessControl +AccessControl.allow_module.__roles__ = () + +# 2. make sure /@@ doesn't traverse to annotations +from zope.traversing import namespace +from zope.traversing.interfaces import TraversalError +old_traverse = namespace.view.traverse +def traverse(self, name, ignored): + if not name: + raise TraversalError(self.context, name) + return old_traverse(self, name, ignored) +namespace.view.traverse = traverse + +# 3. be sure to check Access contents information permission for FTP users +from AccessControl import getSecurityManager +from zExceptions import Unauthorized +from OFS.ObjectManager import ObjectManager +ObjectManager.__old_manage_FTPlist = ObjectManager.manage_FTPlist +def manage_FTPlist(self, REQUEST): + """Returns a directory listing consisting of a tuple of + (id,stat) tuples, marshaled to a string. Note, the listing it + should include '..' if there is a Folder above the current + one. + + In the case of non-foldoid objects it should return a single + tuple (id,stat) representing itself.""" + + if not getSecurityManager().checkPermission('Access contents information', self): + raise Unauthorized('Not allowed to access contents.') + + return self.__old_manage_FTPlist(REQUEST) +ObjectManager.manage_FTPlist = manage_FTPlist + +# 4. Make sure z3c.form widgets don't get declared as public +from Products.Five.metaconfigure import ClassDirective +old_require = ClassDirective.require +def require(self, *args, **kw): + if self._ClassDirective__class.__module__.startswith('z3c.form.browser'): + return + return old_require(self, *args, **kw) +ClassDirective.require = require
Products/CMFPlone/PloneControlPanel.py+1 −0 modified@@ -267,6 +267,7 @@ def addAction(self, return self.manage_editActionsForm( REQUEST, manage_tabs_message='Added.') + security.declareProtected(ManagePortal, 'registerConfiglet') registerConfiglet = addAction security.declareProtected(ManagePortal, 'manage_editActionsForm')
Products/CMFPlone/PloneTool.py+2 −0 modified@@ -12,6 +12,7 @@ from zope.lifecycleevent import ObjectModifiedEvent from AccessControl import ClassSecurityInfo, Unauthorized +from AccessControl import getSecurityManager from Acquisition import aq_base from Acquisition import aq_inner from Acquisition import aq_parent @@ -1312,6 +1313,7 @@ def renameObjectsByPaths(self, paths, new_ids, new_titles, change_title = new_title and title != new_title changed = False if change_title: + getSecurityManager().validate(obj, obj, 'setTitle', obj.setTitle) obj.setTitle(new_title) notify(ObjectModifiedEvent(obj)) changed = True
Products/CMFPlone/skins/plone_scripts/createObject.cpy+2 −0 modified@@ -19,6 +19,8 @@ response.setHeader('Cache-Control', 'no-cache') if id is None: id = context.generateUniqueId(type_name) +else: + id = id.replace('$', '$$') if type_name is None: raise Exception('Type name not specified')
Products/CMFPlone/skins/plone_scripts/formatColumns.py+4 −0 modified@@ -3,6 +3,10 @@ # returns a list of lists of items +from zExceptions import Forbidden +if container.REQUEST.get('PUBLISHED') is script: + raise Forbidden('Script may not be published.') + rows = [] i = 0
Products/CMFPlone/skins/plone_scripts/getFolderContents.py+4 −0 modified@@ -10,6 +10,10 @@ # NOTE: This script is obsolete, use the browser view # @@folderListing in plone.app.contentlisting +from zExceptions import Forbidden +if container.REQUEST.get('PUBLISHED') is script: + raise Forbidden('Script may not be published.') + mtool = context.portal_membership cur_path = '/'.join(context.getPhysicalPath()) path = {}
Products/CMFPlone/skins/plone_scripts/translate.py+4 −0 modified@@ -6,6 +6,10 @@ ##bind subpath=traverse_subpath ##parameters=msgid, mapping={}, default=None, domain='plone', target_language=None, escape_for_js=False +from zExceptions import Forbidden +if container.REQUEST.get('PUBLISHED') is script: + raise Forbidden('Script may not be published.') + # handle the possible "nothing" condition in folder_contents.pt ln 21 # gracefully if msgid == None:
Products/CMFPlone/skins/plone_scripts/utranslate.py+4 −0 modified@@ -6,6 +6,10 @@ ##bind subpath=traverse_subpath ##parameters=msgid, mapping={}, default=None, domain='plone', target_language=None, escape_for_js=False +from zExceptions import Forbidden +if container.REQUEST.get('PUBLISHED') is script: + raise Forbidden('Script may not be published.') + # handle the possible "nothing" condition in folder_contents.pt ln 21 # gracefully if msgid == None:
Products/CMFPlone/tests/testSecurity.py+220 −0 added@@ -0,0 +1,220 @@ +import re +import unittest +from urllib import urlencode +from Testing.makerequest import makerequest +from Products.PloneTestCase import PloneTestCase as ptc +from Products.Five.testbrowser import Browser +from zExceptions import Unauthorized + +ptc.setupPloneSite() + + +class TestAttackVectorsUnit(unittest.TestCase): + + def test_gtbn_funcglobals(self): + from Products.CMFPlone.utils import getToolByName + try: + getToolByName(self.assertTrue,'func_globals')['__builtins__'] + except TypeError: + pass + else: + self.fail('getToolByName should block access to non CMF tools') + + def test_setHeader_drops_LF(self): + from ZPublisher.HTTPResponse import HTTPResponse + response = HTTPResponse() + response.setHeader('Location', + 'http://www.ietf.org/rfc/\nrfc2616.txt') + self.assertEqual(response.headers['location'], + 'http://www.ietf.org/rfc/rfc2616.txt') + + def test_PT_allow_module_not_available_in_RestrictedPython_1(self): + src = ''' +from AccessControl import Unauthorized +try: + import Products.PlacelessTranslationService +except (ImportError, Unauthorized): + raise AssertionError("Failed to import Products.PTS") +Products.PlacelessTranslationService.allow_module('os') +''' + from Products.PythonScripts.PythonScript import PythonScript + script = makerequest(PythonScript('script')) + script._filepath = 'script' + script.write(src) + self.assertRaises((ImportError, Unauthorized), script) + + def test_PT_allow_module_not_available_in_RestrictedPython_2(self): + src = ''' +from Products.PlacelessTranslationService import allow_module +allow_module('os') +''' + from Products.PythonScripts.PythonScript import PythonScript + script = makerequest(PythonScript('script')) + script._filepath = 'script' + script.write(src) + self.assertRaises((ImportError, Unauthorized), script) + + def test_get_request_var_or_attr_disallowed(self): + import App.Undo + self.assertFalse(hasattr(App.Undo.UndoSupport, 'get_request_var_or_attr')) + + +class TestAttackVectorsFunctional(ptc.FunctionalTestCase): + + def test_widget_traversal_1(self): + res = self.publish('/plone/@@discussion-settings/++widget++moderator_email') + self.assertEqual(302, res.status) + self.assertTrue(res.headers['location'].startswith('http://nohost/plone/acl_users/credentials_cookie_auth/require_login')) + + def test_widget_traversal_2(self): + res = self.publish('/plone/@@discussion-settings/++widget++captcha/terms/field/interface/setTaggedValue?tag=cake&value=lovely') + self.assertEqual(302, res.status) + self.assertTrue(res.headers['location'].startswith('http://nohost/plone/acl_users/credentials_cookie_auth/require_login')) + + def test_registerConfiglet_1(self): + VECTOR = "/plone/portal_controlpanel/registerConfiglet?id=cake&name=Cakey&action=woo&permission=View&icon_expr=" + res = self.publish(VECTOR) + self.assertTrue(res.headers['location'].startswith('http://nohost/plone/acl_users/credentials_cookie_auth/require_login')) + + def test_registerConfiglet_2(self): + VECTOR = "/plone/portal_controlpanel/registerConfiglet?id=cake&name=Cakey&action=woo&permission=View&icon_expr=" + self.publish(VECTOR) + action_ids = [action.id for action in self.portal.portal_controlpanel._actions] + self.assertTrue('cake' not in action_ids) + + def _get_authenticator(self, basic=None): + url = '/plone/login_password' + res = self.publish(url, basic=basic) + m = re.search('name="_authenticator" value="([^"]*)"', res.body) + if m: + return m.group(1) + return '' + + def test_renameObjectsByPaths(self): + PAYLOAD = { + 'paths:list': '/plone/news', + # id must stay the same + 'new_ids:list': 'news', + 'new_titles:list': 'EVIL', + # Set orig_template to 'view'. Otherwise folder_rename "success" redirects + # to folder_contents, which will raise Unauthorized. + 'orig_template': 'view', + } + + browser = Browser() + csrf_token = self._get_authenticator() + + PAYLOAD['_authenticator'] = csrf_token + # Call folder_rename anywhere + browser.open('http://nohost/plone/folder_rename', + urlencode(PAYLOAD)) + self.assertTrue('The following item(s) could not be renamed: /plone/news.' in browser.contents) + self.assertEqual('News', self.portal.news.Title()) + + def test_renameObjectByPaths_postonly(self): + from Products.PythonScripts.PythonScript import PythonScript + script = PythonScript('script') + script._filepath = 'script' + src = """context.plone_utils.renameObjectsByPaths(paths=['/plone/news'], new_ids=['news'], new_titles=['EVIL'], REQUEST=context.REQUEST)""" + script.write(src) + self.portal.evil = script + csrf_token = self._get_authenticator() + + self.publish('/plone/evil', extra={'_authenticator': csrf_token}, request_method='POST') + self.assertEqual('News', self.portal.news.Title()) + + owner_basic = ptc.portal_owner + ':' + ptc.default_password + csrf_token = self._get_authenticator(owner_basic) + self.publish('/plone/evil', extra={'_authenticator': csrf_token}, basic=owner_basic) + self.assertEqual('News', self.portal.news.Title()) + self.publish('/plone/evil', request_method='POST', extra={'_authenticator': csrf_token}, basic=owner_basic) + self.assertEqual('EVIL', self.portal.news.Title()) + + self.setRoles(['Manager']) + self.portal.news.setTitle('News') + self.portal.plone_utils.renameObjectsByPaths(paths=['/plone/news'], new_ids=['news'], new_titles=['EVIL']) + self.assertEqual('EVIL', self.portal.news.Title()) + self.portal.news.setTitle('News') + + self.setRoles(['Member']) + self.portal.plone_utils.renameObjectsByPaths(paths=['/plone/news'], new_ids=['news'], new_titles=['EVIL']) + self.assertEqual('News', self.portal.news.Title()) + + def test_gtbn_faux_archetypes_tool(self): + from Products.CMFPlone.FactoryTool import FauxArchetypeTool + from Products.CMFPlone.utils import getToolByName + self.portal.portal_factory.archetype_tool = FauxArchetypeTool(self.portal.archetype_tool) + self.assertEqual(self.portal.portal_factory.archetype_tool, getToolByName(self.portal.portal_factory, 'archetype_tool')) + + def test_searchForMembers(self): + res = self.publish('/plone/portal_membership/searchForMembers') + self.assertEqual(302, res.status) + self.assertTrue(res.headers['location'].startswith('http://nohost/plone/acl_users/credentials_cookie_auth/require_login')) + + def test_getMemberInfo(self): + res = self.publish('/plone/portal_membership/getMemberInfo?id=admin') + self.assertEqual(404, res.status) + + def test_queryCatalog(self): + res = self.publish('/plone/news/aggregator/queryCatalog') + self.assertEqual(404, res.status) + + def test_resolve_url(self): + res = self.publish("/plone/uid_catalog/resolve_url?path=/evil") + self.assertEqual(302, res.status) + self.assertTrue(res.headers['location'].startswith('http://nohost/plone/acl_users/credentials_cookie_auth/require_login')) + + def test_at_download(self): + self.setRoles(['Manager']) + self.portal.portal_workflow.setChainForPortalTypes(['File'], 'plone_workflow') + self.portal.invokeFactory('File', 'test') + self.portal.portal_workflow.doActionFor(self.portal.test, 'publish') + + # give it a more restricted read_permission + self.portal.test.Schema()['file'].read_permission = 'Manage portal' + + # make sure at_download disallows even though the user has View permission + res = self.publish('/plone/test/at_download/file') + self.assertEqual(res.status, 302) + self.assertTrue(res.headers['location'].startswith('http://nohost/plone/acl_users/credentials_cookie_auth/require_login')) + + def test_ftp(self): + self.setRoles(['Manager', 'Owner']) + self.portal.REQUEST.PARENTS = [self.app] + res = self.portal.news.manage_FTPlist(self.portal.REQUEST) + self.assertTrue(isinstance(res, basestring)) + self.portal.portal_workflow.doActionFor(self.portal.news, 'hide') + self.setRoles(['Member']) + from zExceptions import Unauthorized + self.assertRaises(Unauthorized, self.portal.news.manage_FTPlist, self.portal.REQUEST) + + def test_atat_does_not_return_anything(self): + res = self.publish('/plone/@@') + self.assertEqual(404, res.status) + + def test_go_back(self): + res = self.publish('/plone/front-page/go_back?last_referer=http://${request}', + basic=ptc.portal_owner + ':' + ptc.default_password) + self.assertEqual(302, res.status) + self.assertEqual('http://${request}', res.headers['location'][:17]) + + def test_getFolderContents(self): + res = self.publish('/plone/getFolderContents') + self.assertEqual(403, res.status) + + def test_translate(self): + res = self.publish('/plone/translate?msgid=foo') + self.assertEqual(403, res.status) + + def test_utranslate(self): + res = self.publish('/plone/utranslate?msgid=foo') + self.assertEqual(403, res.status) + + def test_createObject(self): + res = self.publish('/plone/createObject?type_name=File&id=${foo}') + self.assertEqual(302, res.status) + self.assertEqual('http://nohost/plone/portal_factory/File/${foo}/edit', res.headers['location']) + + def test_formatColumns(self): + res = self.publish('/plone/formatColumns?items:list=') + self.assertEqual(403, res.status)
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
13- plone.org/products/plone-hotfix/releases/20121106nvdPatchWEB
- github.com/advisories/GHSA-cxw7-85xm-3xrcghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2012-5488ghsaADVISORY
- plone.org/products/plone/security/advisories/20121106/04nvdVendor AdvisoryWEB
- rhn.redhat.com/errata/RHSA-2014-1194.htmlnvdWEB
- www.openwall.com/lists/oss-security/2012/11/10/1nvdWEB
- access.redhat.com/errata/RHSA-2014:1194ghsaWEB
- access.redhat.com/security/cve/CVE-2012-5488ghsaWEB
- bugzilla.redhat.com/show_bug.cgighsaWEB
- github.com/plone/Products.CMFPlone/blob/4.2.3/docs/CHANGES.txtnvdWEB
- github.com/plone/Products.CMFPlone/commit/a9479a5b38646fe0b0a9066ee46de9c18de32bfaghsaWEB
- github.com/plone/Products.CMFPlone/commit/c3a98f4e6cf26501485de9c8354c49afdea21df8ghsaWEB
- github.com/pypa/advisory-database/tree/main/vulns/plone/PYSEC-2014-30.yamlghsaWEB
News mentions
0No linked articles in our index yet.