AppyPrinciplesGetting started
appy.pod Secure pod

By default, when using pod, expressions and statement parts found in pod templates (be it ODT or ODS) are evaluated using the standard Python eval method.

You may consider it as a security problem. Indeed, some organizations using pod let advanced users or third party companies create or modify pod templates.

This is why it is possible to restrict the use of some expressions and statements via the notion of evaluator.

An evaluator is a component that evaluates all pure Python expressions and statements one may write within pod expressions and statement parts.

The default evaluator, implemented by class Evaluator located in appy/pod/evaluator.py, simply delegates such evaluations to the eval Python fonction to do the job.

The pod renderer has a parameter named evaluator: if set to None (being the default value), the default evaluator will be used. If an instance of one of the classes described below is placed in this attribute, an alternate evaluator will be used.

As described in the following sub-sections, pod offers you two other built-in and configurable evaluators: the Compromiser and a RestrictedPython-based evaluator. Moreover, it is also possible to build your own evaluator.

The Compromiser

Available since Appy 1.0.21.

The Compromiser, implemented by the homonym class in appy/pod/evaluator.py (the same module as the default Evaluator), tries to establish a well-balanced compromise between coders' power and security. Its objective is to let coders express themselves while preventing the use of most (in)famous risky Python functions and statements. The name of this evaluator has also been chosen for is polysemy: by using it, will your production servers be compromised ?

The Compromiser is built in front of the default evaluator: before evaluating any Python expression or statement via Python's eval method, it checks the presence of banned elements in it and raises an exception if one is found.

How to enable it

If you want to enable the Compromiser, place, in attribute evaluator of your renderer's constructor, an instance of class Compromiser, as found in appy/pod/evaluator.py. Here is a basic example.

from appy.pod.renderer import Renderer
from appy.pod.evaluator import Compromiser
...
renderer = Renderer(..., evaluator=Compromiser(), ...)
...

The RestrictedPython-based evaluator

Available since Appy 1.0.13.

For paranoia enthusiasts or large development teams, module appy/pod/restricted.py proposes an evaluator that integrates RestrictedPython.

The Renderer's constructor (in appy/pod/renderer.py) has an argument named evaluator. By default, it is set to None. In that case, a standard, eval-based evaluator will be used (from appy/pod/evaluator.py).

Alternately, you can place, in this arg, an instance of class Evaluator as defined in module appy/pod/restricted.py. This class' constructor looks as follows (basic RestrictedPython knowlegde is a prerequisite to understand it).

    def __init__(self, builtins=None, write=None, item=None, unpack=None,
                 iterUnpack=None, attr=None, custom=None):
        # Raise an error if RestrictedPython is not installed
        if not installed:
            raise Exception(RP_N_INS)
        # The replacement for Python __builtins__
        self.builtins = builtins or rp.Guards.safe_builtins
        # Prevent write access to objet attributes
        self.write = write or rp.Guards.full_write_guard
        # For "for" statements and comprehensions
        self.item = item or rp.Eval.default_guarded_getitem
        try:
            self.unpack = unpack or rp.Guards.guarded_unpack_sequence
        except AttributeError:
            self.unpack = unpack # Default may not be available
        try:
            self.iterUnpack = iterUnpack or \
                              rp.Guards.guarded_iter_unpack_sequence
        except AttributeError:
            self.iterUnpack = None
        # Prevent using stdout for printing
        self.safePrint = rp.PrintCollector
        # Protected "getattr". Any other attribute than attr will be
        # initialised once, globally, via updateContext, before any expression
        # is evaluated. attr, on the contrary, will be injected in the
        # context, in run, every time an expression is evaluated.
        self.attr = attr
        # If custom is None, self's attributes as defined hereabove will be
        # injected (by updateContext) in the evaluation context of any
        # evaluated expression, at standard RestrictedPython keys as defined in
        # the v_contextMap. Alternately, if you want to have full control over
        # the RestrictedPython-specific entries you want to inject in the
        # context of any evaluated expression, define them in a custom dict.
        # In this latter case, self's attributes will be ignored: entries from
        # the custom dict will be used instead. This option is useful if the
        # framework you use in conjunction with pod already defines a function
        # that computes such entries.
        self.custom = custom

Basically, constructor's arguments allow to provide alternate, secure elements, that will be injected in the evaluation context of any expression that will be evaluated by pod. Then, every time pod will need to evaluate a Python expression, the evaluator will call RestrictedPython, that will evaluate it in this updated, secured context. Arg builtins, for example, defines a sub-set of standard Python __builtins__. Because, for every element for which a secure replacement may be used, several functions may be proposed (either by RestrictedPython itself or by third-party frameworks), the Evaluator constructor proposes args allowing to provide specific values, fallbacking to default ones if no value is passed. For example, RestrictedPython provides several variants for the __builtins__ sub-set: safe_builtins, limited_builtins or utility_builtins. appy.pod.restricted.Evaluator's default one is safe_builtins.

An exemple: using RestrictedPython from Zope

For those using the Zope application framework, here is an example of how to configure Zope-specific RestrictedPython rules.

By default, an instance of class appy.pod.restricted.Evaluator builds, from its attributes, a dict of all the RestrictedPython-specific entries that will need to be injected in the context of any expression to be evaluated via pod. The problem is that Zope already provides a function that builds such dict: AccessControl.ZopeGuards.get_safe_globals. This is why Evaluator's custom attribute has been foreseen: if not empty, all other Evaluator attributes will be ignored: the custom dict being used instead. Moreover, Zope also provides his own function for replacing the standard getattr function, in AccessControl.ZopeGuards.guarded_getattr. Consequently, here is how to define an Evaluator instance to be used from Zope.

from AccessControl.ZopeGuards import get_safe_globals
from AccessControl.ZopeGuards import guarded_getattr
 
from appy.pod.renderer import Renderer
from appy.pod.restricted import Evaluator
 
rpEvaluator = Evaluator(attr=guarded_getattr, custom=get_safe_globals())
 
# Then, pass the evaluator to the Renderer
renderer = Renderer(..., evaluator=rpEvaluator, ...)

Build your own evaluator

You may build your own evaluator by sub-classing any existing Evaluator class, be it the base Evaluator class itself or any of its existing sub-classes.

Your sub-class may override any of the 2 methods defined on the base Evaluator class, whose signature and implementation are shown below.

    def run(self, expression, context):
        '''Evaluates expression in this context'''
        # context can be a standard dict or an instance of class
        # appy.model.utils.Object. In this latter case, although it implements
        # dict-like methods, we prefer to unwrap its dict instead of using it
        # directly as context, because it does not raise a KeyError when a key
        # lookup produces no result, but returns None instead.
        context = context if isinstance(context, dict) else context.__dict__
        # Evaluate expression
        return eval(expression, None, context)
        # context is passed as locals, in order to avoid the "locals" dict to
        # be cloned by the eval function (see https://peps.python.org/pep-0667).
        # Before, v_context was passed as globals and, in that case, the "eval"
        # function added, within it, if not already present, Python built-ins
        # at key '__builtins__'. So, v_context['__builtins__'] was similar to
        # the homonym entry in dict globals().

    def updateContext(self, context):
        '''This standard evaluator does not need to update the context'''

Here is an example.

#- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
from appy.pod.evaluator import Evaluator
 
class MyEvaluator(Evaluator):
    '''My first evaluator'''
 
    def checkExpression(self, expression, context):
        '''Ensures this p_expression, as found in a pod expression or statement,
           can securely be evaluated in the current p_context.'''
        ok = ...
        return ok
 
    def run(self, expression, context):
        '''Will be called everytime an p_expression is encountered in a pod
           template, with that p_context, being a dict as built by the pod
           renderer.'''
        # My own check
        ok = self.checkExpression(expression, context)
        if not ok: raise Exception('Goddamned')
        return super().run(expression, context)
 
    def secureTouchy(self, *args, **kwargs):
        '''Secure variant for some touchy method'''
        ...
 
    def updateContext(self, context):
        '''Will be called once, when the renderer starts rendering a pod
           template: it allows to patch the renderer p_context.'''
        # Give my own implementation for the 'touchy' method
        context['touchy'] = self.secureTouchy
#- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -