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):
if not installed:
raise Exception(RP_N_INS)
self.builtins = builtins or rp.Guards.safe_builtins
self.write = write or rp.Guards.full_write_guard
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
self.safePrint = rp.PrintCollector
self.attr = attr
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 = context if isinstance(context, dict) else context.__dict__
return eval(expression, None, context)
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
#- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -