What I mean with Restricted URL Traversal is the possibility to prevent cross site access in Zope. Let's say we have the following structure:
root
|
|____ Folder1 (Root for www.domain1.tld)
| |
| |____ index_html
| |
| |____ images
|
|____ Folder2 (Root for www.domain2.tld)
|
|____ index_html
Because of Zope's path traversal and acquisition it is possible to access the content of Folder1 under the URL http://www.domain2.tld/Folder1. How to prevent this behaviour as centralized as possible?
Well, in Zope you can set Access Rules for objects in the ZMI. These Access Rules are executed on each request before all traversal publishing is done. Therefore you do not know anything about the final context or the authentication. That is what we have to change first by use of a Post Traversal Hook.
Under the root folder of your ZMI create a Script (Python) and name it access_rule. Give it two parameters (folder, request) and the following body:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 | ## Script (Python) "access_rule"
##bind container=container
##bind context=context
##bind namespace=
##bind script=script
##bind subpath=traverse_subpath
##parameters=folder, request
##title=
##
from AccessControl import getSecurityManager
def not_found(obj):
obj_id = obj.getId()
resource = request['URL'].split('/%s/' % obj_id, 1)
if len(resource) == 2:
resource[1] = obj_id
resource = '/'.join(resource)
# get default error message; also sets 404 status
try:
request.RESPONSE.notFoundError(resource)
except Exception, err:
msg = str(err)
# try to get customized error message
parent = obj.aq_parent
try:
msg = parent.standard_error_message(parent, request,
error_type='NotFound', error_value=obj_id,
error_message=msg)
except:
pass
return msg
def call():
# Get parents from top to bottom
PARENTS = request.PARENTS[::-1]
blocked = []
# Iterate through parents from top to bottom
for obj in PARENTS:
# Look for a post_traverse_access_rule object
try:
if not (getattr(obj, 'objectIds', None) \
and 'post_traverse_access_rule' in obj.objectIds()):
continue
except:
continue
# Do not execute again by default
if obj in blocked:
continue
blocked.append(obj)
# Call it and catch error if any
try:
retval = obj.post_traverse_access_rule()
except Exception, err:
errors = request.get('access_rule_errors', [])
errors.append((obj, err))
request.set('access_rule_errors', errors)
continue
if not same_type(retval, {}):
continue
if retval.get('traversal_repeating'):
blocked.remove(obj)
# Exclude users with manager role
if not retval.get('apply_to_manager'):
user = getSecurityManager().getUser()
if user.has_role('Manager'):
continue
# Verify hostnames
hosts = retval.get('restrict_hostnames')
if hosts:
if same_type(hosts, ''):
hosts = hosts.split()
for host in hosts:
if request['SERVER_URL'].endswith(host):
break
else:
return not_found(obj)
# Get object in required context
index = PARENTS.index(obj)
obj = PARENTS[index]
# Split object path
parents = PARENTS[:index]
children = PARENTS[index:]
try:
children.append(request.PUBLISHED)
except:
pass
# Restrict parent traversal
restricted = retval.get('restrict_parent_traversal')
if restricted:
if same_type(restricted, ''):
restricted = restricted.split()
elif not (same_type(restricted, ()) or same_type(restricted, [])):
restricted = []
path_parent = PARENTS[-1].absolute_url_path()
for path in restricted:
if path[0] != '/':
path = '/' + path
if path_parent.startswith(path):
break
else:
for child in children:
if child is obj:
continue
try:
parent = child.aq_inner.aq_parent
except:
continue
if parent in parents:
return not_found(child)
# Check for indirect traversal
direct = retval.get('force_direct_traversal')
if direct:
objects = []
if direct in ('parents', 'both'):
objects.extend(parents)
objects.append(obj)
if direct in ('children', 'both'):
objects.extend(children)
# Compare parent containers with aq_parent objects
for i in range(len(objects)-1):
child = objects[i+1]
try:
parent = child.aq_inner.aq_parent
except:
continue
if parent is not objects[i]:
return not_found(child)
# Redirect
url = retval.get('redirect_url')
if url:
request.RESPONSE.redirect(url)
if not retval.get('return_value'):
return 'Redirecting to ' + url
# Return value
value = retval.get('return_value')
if value:
if same_type(value, Exception()):
raise value
return value
try:
# Register post traversal hook
request.post_traverse(call, ())
except:
pass
|
Now make it an Access Rule as explained here. After that you have something like the precondition attribute of a file object. Just add a Script (Python) with the id post_traverse_access_rule to a folder you want to control and it will be called on each request of any object under that folder. The script should return a dictionary which can contain the following attributes:
- traversal_repeating
If set to true, your script will be called each time the traversal machinery comes upon the container of your script.
- apply_to_manager
If set to true, return values of your script will be applied also when the user has a manager role, otherwise they are ignored. Use with care!
- restrict_hostnames
Could be set to a list or whitespace seperated string of allowed hostnames (eg. ['domain1.com', 'www.domain2.net', 'ssl.domain3.org']). Each request with an invalid hostname gets a NotFound error message.
- restrict_parent_traversal
If set to true, you cannot traverse upon that folder. A list of allowed paths can be passed.
- force_direct_traversal
Can be set to parents, children or both and prevents cross traversal.
- redirect_url
This attribute specifies a URL the user will be redirected to.
- return_value
Set this attribute if you want to return a value. Is it an exception it will be raised. Note that this value will be the final response to the user.
Any return value other than a dictionary will be ignored because of the ability to lock yourself out of Zope. Exceptions are collected and added to the REQUEST object (access_rule_errors). Should you lock yourself out of Zope you can try to disable the Access Rule by adding _SUPPRESS_ACCESSRULE to the URL or restart Zope with "suppress-all-access-rules on" in zope.conf.