joonis Logo

Restricted URL Traversal in Zope

What is meant by 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.

Share it Facebook Twitter Google+ Email AddThis

Kann ich
  Ihnen helfen?


Schreiben Sie mir
doch einfach unter
giraffe@joonis.de