Commit 0e32c197 authored by Michael Howitz's avatar Michael Howitz Committed by GitHub

Add ability to paste objects without limits. [2.13] (#270)

* Add tests to check the DoS prevention code.

* Add `._pasteObjects()` to be able to paste objects without limits.

Fixes #217.
parent a819388c
[buildout] [buildout]
extensions = mr.developer extensions = mr.developer
index = https://pypi.python.org/simple/ index = https://pypi.org/simple/
allow-hosts =
*.python.org
*.zope.org
argparse.googlecode.com
show-picked-versions = true show-picked-versions = true
always-accept-server-certificate = true always-accept-server-certificate = true
develop = . develop = .
......
...@@ -8,7 +8,9 @@ http://docs.zope.org/zope2/ ...@@ -8,7 +8,9 @@ http://docs.zope.org/zope2/
2.13.28 (unreleased) 2.13.28 (unreleased)
-------------------- --------------------
- ... - Add ``OFS.CopySupport.CopyContainer._pasteObjects()`` to be able to paste
objects no matter how many objects where cut or copied.
(`#217 <https://github.com/zopefoundation/Zope/issues/217>`_)
2.13.27 (2018-01-27) 2.13.27 (2018-01-27)
......
...@@ -169,28 +169,29 @@ class CopyContainer(Base): ...@@ -169,28 +169,29 @@ class CopyContainer(Base):
id='copy%s_of_%s' % (n and n+1 or '', orig_id) id='copy%s_of_%s' % (n and n+1 or '', orig_id)
n=n+1 n=n+1
security.declareProtected(view_management_screens, 'manage_pasteObjects') def _pasteObjects(self, cp, cb_maxsize=0):
def manage_pasteObjects(self, cb_copy_data=None, REQUEST=None):
"""Paste previously copied objects into the current object. """Paste previously copied objects into the current object.
If calling manage_pasteObjects from python code, pass the result of a ``cp`` is the list of objects for paste as encoded by ``_cb_encode``.
If calling _pasteObjects from python code, pass the result of a
previous call to manage_cutObjects or manage_copyObjects as the first previous call to manage_cutObjects or manage_copyObjects as the first
argument. argument.
Also sends IObjectCopiedEvent and IObjectClonedEvent ``cb_maxsize`` is the maximum size of the JSON representation of the
object list. Set it to a non-zero value to prevent DoS attacks with
huge object lists or zlib bombs.
This method sends IObjectCopiedEvent and IObjectClonedEvent
or IObjectWillBeMovedEvent and IObjectMovedEvent. or IObjectWillBeMovedEvent and IObjectMovedEvent.
Returns tuple of (operator, list of {'id': orig_id, 'new_id': new_id}).
Where `operator` is 0 for a copy operation and 1 for a move operation.
""" """
if cb_copy_data is not None:
cp = cb_copy_data
elif REQUEST is not None and REQUEST.has_key('__cp'):
cp = REQUEST['__cp']
else:
cp = None
if cp is None: if cp is None:
raise CopyError(eNoData) raise CopyError(eNoData)
try: try:
op, mdatas = _cb_decode(cp) op, mdatas = _cb_decode(cp, cb_maxsize)
except: except:
raise CopyError(eInvalid) raise CopyError(eInvalid)
...@@ -243,10 +244,6 @@ class CopyContainer(Base): ...@@ -243,10 +244,6 @@ class CopyContainer(Base):
notify(ObjectClonedEvent(ob)) notify(ObjectClonedEvent(ob))
if REQUEST is not None:
return self.manage_main(self, REQUEST, update_menu=1,
cb_dataValid=1)
elif op == 1: elif op == 1:
# Move operation # Move operation
for ob in oblist: for ob in oblist:
...@@ -311,13 +308,40 @@ class CopyContainer(Base): ...@@ -311,13 +308,40 @@ class CopyContainer(Base):
# try to make ownership implicit if possible # try to make ownership implicit if possible
ob.manage_changeOwnershipType(explicit=0) ob.manage_changeOwnershipType(explicit=0)
if REQUEST is not None: return op, result
security.declareProtected(view_management_screens, 'manage_pasteObjects')
def manage_pasteObjects(self, cb_copy_data=None, REQUEST=None):
"""Paste previously copied objects into the current object.
If calling manage_pasteObjects from python code, pass the result of a
previous call to manage_cutObjects or manage_copyObjects as the first
argument.
Also sends IObjectCopiedEvent and IObjectClonedEvent
or IObjectWillBeMovedEvent and IObjectMovedEvent.
If `REQUEST` is None it returns a
list of dicts {'id': orig_id, 'new_id': new_id} otherwise it renders
a HTML page.
"""
if (cb_copy_data is None and
REQUEST is not None and
REQUEST.has_key('__cp')):
cb_copy_data = REQUEST['__cp']
op, result = self._pasteObjects(cb_copy_data, cb_maxsize=8192)
if REQUEST is not None:
if op == 0:
cb_valid = 1
elif op == 1:
REQUEST['RESPONSE'].setCookie('__cp', 'deleted', REQUEST['RESPONSE'].setCookie('__cp', 'deleted',
path='%s' % cookie_path(REQUEST), path='%s' % cookie_path(REQUEST),
expires='Wed, 31-Dec-97 23:59:59 GMT') expires='Wed, 31-Dec-97 23:59:59 GMT')
REQUEST['__cp'] = None REQUEST['__cp'] = None
return self.manage_main(self, REQUEST, update_menu=1, cb_valid = 0
cb_dataValid=0) return self.manage_main(self, REQUEST, update_menu=1,
cb_dataValid=cb_valid)
return result return result
...@@ -684,6 +708,13 @@ def _cb_encode(d): ...@@ -684,6 +708,13 @@ def _cb_encode(d):
return quote(compress(dumps(d), 9)) return quote(compress(dumps(d), 9))
def _cb_decode(s, maxsize=8192): def _cb_decode(s, maxsize=8192):
"""Decode a list of IDs from storage in a cookie.
``s`` is text as encoded by ``_cb_encode``.
``maxsize`` is the maximum size of uncompressed data. ``0`` means no limit.
Return a list of text IDs.
"""
dec = decompressobj() dec = decompressobj()
data = dec.decompress(unquote(s), maxsize) data = dec.decompress(unquote(s), maxsize)
if dec.unconsumed_tail: if dec.unconsumed_tail:
......
import random
import string
import unittest import unittest
import cStringIO import cStringIO
...@@ -283,6 +285,40 @@ class TestCopySupport( CopySupportTestBase ): ...@@ -283,6 +285,40 @@ class TestCopySupport( CopySupportTestBase ):
{'id':'file1', 'new_id':'copy_of_file1'}, {'id':'file1', 'new_id':'copy_of_file1'},
{'id':'file2', 'new_id':'copy_of_file2'}]) {'id':'file2', 'new_id':'copy_of_file2'}])
def assertCopyError(self, callable, error_text, *args):
from OFS.CopySupport import CopyError
try:
callable(*args)
except CopyError as err:
if error_text:
self.assertTrue(error_text in str(err))
else:
self.fail('CopyError not raised.')
def testPasteNoData(self):
self.assertCopyError(self.folder1.manage_pasteObjects, '')
def testPasteTooBigData(self):
from OFS.CopySupport import _cb_encode
def make_data(lenght):
return _cb_encode(
(1, [''.join(random.sample(string.printable, 20))
for x in range(lenght)]))
# Protect against DoS attack with too big data:
self.assertCopyError(
self.folder1.manage_pasteObjects, 'Clipboard Error',
make_data(350))
# But not too much data is allowed:
self.assertCopyError(
self.folder1.manage_pasteObjects, 'Item Not Found',
make_data(300))
# _pasteObjects allows to paste without restriction:
self.assertCopyError(
self.folder1._pasteObjects, 'Item Not Found', make_data(3500))
class _SensitiveSecurityPolicy: class _SensitiveSecurityPolicy:
def __init__( self, validate_lambda, checkPermission_lambda ): def __init__( self, validate_lambda, checkPermission_lambda ):
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment