TransitionToSecurityProxies

Transition to security proxies

This document describes the recent change to the Zope 3 security system introducing name-based permission settings and security proxies.

Impatient readers can skip to the second section from the end of the document to get a quick summary of needed changes to application code.

Previous to these changes, programmers specified permissions for objects, typically by specifying the permissions needed to access instances of classes and their methods. Special code interpreters made calls to security managers to validate access to objects. The security managers checked for __permission__ attributes and disallowed access if the security context (e.g. the authenticated user) didn't have the necessary permission. This approach had a number of drawbacks:

  1. It required storing __permission__ attributes on objects. This is a bit invasive, at least in principle.
  2. Objects that could not have __permission__ attributes, like strings, numbers, lists, and so on, could not be accessed in untrusted contexts.
  3. Special untrusted interpreters were needed. This represented a major implementation burden, especially for creating untrusted versions of Python or Perl. The untrusted (restricted) Python implementation has proven to be a major source of instability in Zope 2.
  4. Security holes could arise from calling trusted code from untrusted code. Malicious untrusted code could trick trusted code into performing unauthorized operations by passing it unexpected objects.

To address these issues, two significant changes have been introduced:

  • Security proxies
  • Name-based permissions

Security Proxies

Security proxies surround objects to prevent unauthorized access. Untrusted code is only allowed to access basic objects, such as strings and numbers, or security proxies.

When an object is passed to untrusted code, it is wrapped in a security proxy unless it is already wrapped. Security proxies mediate all accesses to the wrapped object. Operations on security proxies return security proxies as well. Security proxies passed from untrusted code to trusted code remain wrapped, so untrusted code can't trick trusted code into performing operations that the untrusted code could not perform.

With care, trusted code can explicitly unwrap security proxies and gain additional access. In particular, security proxies cannot be stored in the Zope object database. If an object wrapped by a security proxy is to be stored in another object, the security proxy must be removed.

When a method is accessed in a security proxy, the method attribute access results in the method wrapped in a security proxy. If the return value is a simple value, like a string or number, it is returned as usual, otherwise, the value will be wrapped in a security proxy and returned.

When an operation, such as call or item access is performed on a security proxy, then access to the corresponding Python special method is checked. For example, for a call operation, the __call__ method will be checked. The return value from the operation is treated in the same way as for function calls.

Security proxies are configured with "Checker" objects. Checker objects can be created using the Zope.Security.Checker.Checker factory. The factory takes a function as an argument. The function will be called with an attribute name and returns one of:

  • A permission id for the permission needed to access the attribute,
  • None, indicating that access is forbidden, or
  • the special value Zope.Security.Checker.CheckerPublic to indicate that the attribute access is always allowed.

Typically, the checker constructor is passed a dictionary get method. For example, to create a Checker that provides access to the foo method and to the call operation only if the security context has the Zope.ManageContent permission, the following code may be used:

       from Zope.Security.Checker import Checker

       checker = Checker({'foo': 'Zope.ManageContent',
                          '__call__':  'Zope.ManageContent'}.get)

In this example, with a few exceptions, access to any other attributes or operations is forbidden.

A proxy for an object can be created explicitly by passing an object and a checker to the proxy factory:

       from Zope.Security.Proxy import ProxyFactory

       proxied_object = ProxyFactory(ob, checker)

Proxies are rarely created explicitly. Rather, proxies are created automatically for objects when necessary. This is done by looking up checkers from a checker registry. For this reason, it's important to register checkers for application classes and modules. This is normally done using the protectClass ZCML directive. Checkers can be created and registered from Python code. Registering checkers for application objects is better done from ZCML.

For low-level objects that are used in a variety of situation, it may not be practical to provide permission-based access control. Examples include lists, dictionaries, tuples and BTrees?. For objects like these, it's best to provide public read-only access by default and rely on application objects to mediate access to their low-level objects. The Zope.Security.Checker module provides APIs? for creating and registering checkers from Python code.

Note that a number of attributes and operations are always public:

  • Comparison (__lt__, __gt__, __le__, __ge__, __eq__, __ne__)
  • Boolean value (__nonzero__)
  • Hash (__hash__)
  • Class and interface assertions (__class__, __implements__)

Access to these objects need not and cannot be controlled.

The "stringification" operators methods (__str__ and __repr__) are forbidden by default and must be controlled to be accessed.

Name-based permissions

As might be obvious from the checker description above, permissions are now associated with names, not values. This has the important benefit that all attributes are protected the same way. Attributes like strings and lists are as easy to protect as methods.

protectClass ZCML directive

Access to instances of a class is controlled with the ZCML protectClass directive, which has changed. For example, to allow access to the title and description attributes, the call operation, and the container interface of a class, a directive like the following might be used:

      <security:protectClass name=".SomeClass.">
         <security:protect
            names="title, description, __call__" 
            permission="Zope.View" />
         <security:protect
            interface="Zope.App.OFS.Container.IContainer.IReadContainer" 
            permission="Zope.View" />
         <security:protect
            interface="Zope.App.OFS.Container.IContainer.IWriteContainer" 
            permission="Zope.ManageContent" />
      </security:protectClass>

The protectClass directive has changed in three important ways:

  • The methods attribute has been renamed to names, since all object attributes are protected by name.
  • The instances subdirective has been eliminated, as it no longer makes any sense.
  • The protectClass directive now applies only to the named class. Subclasses are unaffected. You can explicitly reuse security assertions from a base class by using the like_unto attribute. For example:
            <security:protectClass name=".RootFolder."
                                   like_unto=".Folder." /> 
    

Context wrappers

The Zope.ContextWrapper package provides support for creating and inspecting low-level context wrappers, which are Zope 3's replacement for acquisition wrappers. The facilities in this package do not support security proxies. For this reason, a new module, Zope.Proxy.ContextWrapper has been provided to work with context-wrappers in a security-proxy-aware fashion. See Zope.Proxy.IContextWrapper.py for documentation of the facilities provided by Zope.Proxy.ContextWrapper. Most code currently using Zope.ContextWrapper will need to be changed to use Zope.Proxy.ContextWrapper.

Proxy introspection and removal

The Zope.Proxy.ProxyIntrospection module provides facilities for detecting and removing proxies. The function removeAllProxies can be used to remove proxies from objects prior to calling proxy-unaware facilities and prior to saving objects persistently. For example, while the __class__ and __implements__ attributes are available in proxies, the attribute values themselves are proxies. The Python isinstance builtin function and the interface isImplementedBy method won't work with proxies.

Issues

There are a number of open issues with security proxies.

  • Security proxies tend to spread and show up in unexpected places. The Zope publication process starts by wrapping the root object in a proxy. All non-basic objects accessed during URL traversal and in later processing tend to be proxies. As a result, much computation performed by trusted code is performed on and controlled by proxies. This is good and bad. It's good that trusting trusted code is protected from doing harm by being passed proxies, however, many more security assertions are necessary to control access to objects that were previously uncontrolled.
  • There aren't good rules yet for when it is safe to remove proxy wrappers. It will often be necessary for trusted code to strip objects of their security proxies. There need to be better tools and facilities for doing this safely.
  • To perform security checks, the Zope security policy needs access to object context (i.e. acquisition context). In particular, the containment of proxied objects needs to be available to the security policy so that permission grants and denials can be found.

    It is a bit unclear where the responsibility for tracking object context lies. In Zope 2, object context was maintained fairly ubiquitously by acquisition. In Zope 3, we don't have acquisition and don't currently require objects or containers to manage their own context. Rather, context is managed by traversal facilities. It is likely that this won't be enough now that access to far more objects is checked.

Near future bonus

In the near future, the ZCML view, adapter, and utility directives will gain a permission_id attribute. If this attribute is set, then component methods will be protected and it won't be necessary to use protectClass directives for these components. This should greatly improve the readability of ZCML files.

Summary of changes needed to work with the new security machinery

Here is a summary of changes necessary to existing Zope 3 software:

  • protectClass directives need to be changed:
    • Rename methods attribute to names.
    • Remove instances subdirectives.
  • The protectClass directive now applies only to the named class. Subclasses are unaffected. You can explicitly reuse security assertions from a base class by using the like_unto attribute. For example:
            <security:protectClass name=".RootFolder."
                                   like_unto=".Folder." /> 
    
  • It is likely that additional protectClass directives will be needed, because many accessed are being checked now that weren't being checked before.
  • Code using the Zope.ContextWrapper package need to use Zope.App.ContextWrapper instead.
  • Code that needs access to otherwise forbidden attributes, or that needs to store proxied objects will need to use facilities provided by Zope.Proxy.ProxyIntrospection.
  • The SecurityManagement and SecurityManager packages have moved from Zope.App.Security to Zope.Security.

Summary

I've tried to quickly provide a description of the recent changes to the Zope 3 security system. If you find that something is unclear or incorrect, please comment or ask questions.


gvanrossum (Apr 29, 2002 10:04 am; Comment #1)
Where does "like_unto" come from? Is it valley-speak or biblical language? Either way I think it's too cute...

The Proxy implementation does call the checker for comparisons, __nonzero__, __hash__ (however, it can't wrap the result in a proxy; fortunately that's unnecessary since these always return ints or bools).

There is no technical reason to special-case __class__ (these are new-style classes, where __class__ can be overridden by a property).

If you want to make an exception for something, I'd say always allow __repr__; this helps debugging (it's painful if you get something and you can't see what it is because __repr__ is disallowed).

I haven't figured out how to make the Job Board example work. Maybe Shane's debug hack will help. bwarsaw (Apr 29, 2002 1:27 pm; Comment #2) -- Something nags me:

 >     With care, trusted code can explicitly unwrap security proxies and
 >     gain additional access. In particular, security proxies cannot be
 >     stored in the Zope object database. If an object wrapped by a
 >     security proxy is to be stored in another object, the security proxy
 >     must be removed.
 

Lets say untrusted code is building up a small object containment hierarchy and then wants to save this in the database. This is going to be a problem, unless one of two things happens: 1) setattrs() always unwrap before storing the attribute on the underlying object (this is okay as long as the getattr() always wraps before returning the object; 2) you do a "deep unwrap" before you attempt to store the object in the db. This latter you could probably do with a trusted proxy-aware pickler
but one which defends against sneak attacks that try to execute code during pickling/unpickling.

I'm with Guido: "like_unto" is too cutesy. What about "protect_like"?

poster (Apr 29, 2002 1:39 pm; Comment #3)
 >     - Code using the Zope.ContextWrapper package need to use
 >       Zope.App.ContextWrapper instead.
 

This should be Zope.Proxy.ContextWrapper, right?

hathawsh (Apr 29, 2002 7:04 pm; Comment #4)
 >  Lets say untrusted code is building up a small object containment
 >    hierarchy and then wants to save this in the database.  This is going to
 >    be a problem, unless one of two things happens: 1) setattrs() always
 >    unwrap before storing the attribute on the underlying object (this is
 >    okay as long as the getattr() always wraps before returning the object;
 >    2) you do a "deep unwrap" before you attempt to store the object in the
 >    db.  This latter you could probably do with a trusted proxy-aware
 >    pickler -- but one which defends against sneak attacks that try to
 >    execute code during pickling/unpickling.
 

Note that untrusted code has no ability to set attributes except by asking trusted code to do it, so in the current model, there is no way for untrusted code to change ZODB directly (unless it gets access to an unwrapped mutable object). This does put a burden on trusted code, which now has to be aware of security wrappers in order to store objects that may have come from untrusted code. However, in Zope 2 the vast majority of trusted code only expects strings, numbers, and "None" from untrusted code, and those objects are never wrapped in security.

klm (May 1, 2002 2:57 pm; Comment #5)
like_unto doesn't sound right to me. how about 'as':
    <security:protectClass name=".RootFolder."
                           as=".Folder." />

 == "Protect RootFolder as Folder is protected"...



( 97 subscribers )