1#    Copyright (C) 2013 Jeremy S. Sanders
2#    Email: Jeremy Sanders <jeremy@jeremysanders.net>
3#
4#    This program is free software; you can redistribute it and/or modify
5#    it under the terms of the GNU General Public License as published by
6#    the Free Software Foundation; either version 2 of the License, or
7#    (at your option) any later version.
8#
9#    This program is distributed in the hope that it will be useful,
10#    but WITHOUT ANY WARRANTY; without even the implied warranty of
11#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12#    GNU General Public License for more details.
13#
14#    You should have received a copy of the GNU General Public License along
15#    with this program; if not, write to the Free Software Foundation, Inc.,
16#    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17###############################################################################
18
19"""
20'Safe' python code evaluation
21
22The idea is to examine the compiled ast tree and chack for invalid
23entries
24"""
25
26# don't do this as it affects imported files
27# from __future__ import division
28import ast
29
30from ..compat import cstr, cpy3, cbuiltins
31from .. import qtall as qt
32
33def _(text, disambiguation=None, context='SafeEval'):
34    """Translate text."""
35    return qt.QCoreApplication.translate(context, text, disambiguation)
36
37# blacklist of nodes
38forbidden_nodes = set((
39        ast.Global,
40        ast.Import,
41        ast.ImportFrom,
42        ))
43
44if hasattr(ast, 'Exec'):
45    forbidden_nodes.add(ast.Exec)
46
47# whitelist of allowed builtins
48allowed_builtins = frozenset((
49        'ArithmeticError',
50        'AttributeError',
51        'BaseException',
52        'Exception',
53        'False',
54        'FloatingPointError',
55        'IndexError',
56        'KeyError',
57        'NameError',
58        'None',
59        'OverflowError',
60        'RuntimeError',
61        'StandardError',
62        'StopIteration',
63        'True',
64        'TypeError',
65        'ValueError',
66        'ZeroDivisionError',
67        'abs',
68        'all',
69        'any',
70        'apply',
71        'basestring',
72        'bin',
73        'bool',
74        'bytes',
75        'callable',
76        'chr',
77        'cmp',
78        'complex',
79        'dict',
80        'divmod',
81        'enumerate',
82        'filter',
83        'float',
84        'format',
85        'frozenset',
86        'hash',
87        'hex',
88        'id',
89        'int',
90        'isinstance',
91        'issubclass',
92        'iter',
93        'len',
94        'list',
95        'long',
96        'map',
97        'max',
98        'min',
99        'next',
100        'object',
101        'oct',
102        'ord',
103        'pow',
104        'print',
105        'property',
106        'range',
107        'reduce',
108        'repr',
109        'reversed',
110        'round',
111        'set',
112        'slice',
113        'sorted',
114        'str',
115        'sum',
116        'tuple',
117        'unichr',
118        'unicode',
119        'xrange',
120        'zip'
121        ))
122
123numpy_forbidden = set((
124        'frombuffer',
125        'fromfile',
126        'getbuffer',
127        'getbufsize',
128        'load',
129        'loads',
130        'loadtxt',
131        'ndfromtxt',
132        'newbuffer',
133        'pkgload',
134        'recfromcsv',
135        'recfromtxt',
136        'save',
137        'savetxt',
138        'savez',
139        'savez_compressed',
140        'setbufsize',
141        'seterr',
142        'seterrcall',
143        'seterrobj',
144        ))
145
146# blacklist using whitelist above
147forbidden_builtins = ( set(cbuiltins.__dict__.keys()) - allowed_builtins |
148                       numpy_forbidden )
149
150class SafeEvalException(Exception):
151    """Raised by safety errors in code."""
152    pass
153
154class CheckNodeVisitor(ast.NodeVisitor):
155    """Visit ast nodes to look for unsafe entries."""
156
157    def generic_visit(self, node):
158        if type(node) in forbidden_nodes:
159            raise SafeEvalException(_("%s not safe") % type(node))
160        ast.NodeVisitor.generic_visit(self, node)
161
162    def visit_Name(self, name):
163        if name.id[:2] == '__' or name.id in forbidden_builtins:
164            raise SafeEvalException(
165                _('Access to special names not allowed: "%s"') % name.id)
166        self.generic_visit(name)
167
168    def visit_Call(self, call):
169        if not hasattr(call.func, 'id'):
170            raise SafeEvalException(_("Function has no identifier"))
171
172        if call.func.id[:2] == '__' or call.func.id in forbidden_builtins:
173            raise SafeEvalException(
174                _('Access to special functions not allowed: "%s"') %
175                call.func.id)
176        self.generic_visit(call)
177
178    def visit_Attribute(self, attr):
179        if not hasattr(attr, 'attr'):
180            raise SafeEvalException(_('Access denied to attribute'))
181        if ( attr.attr[:2] == '__' or attr.attr[:5] == 'func_' or
182             attr.attr[:3] == 'im_' or attr.attr[:3] == 'tb_' ):
183            raise SafeEvalException(
184                _('Access to special attributes not allowed: "%s"') %
185                attr.attr)
186        self.generic_visit(attr)
187
188def compileChecked(code, mode='eval', filename='<string>',
189                   ignoresecurity=False):
190    """Compile code, checking for security errors.
191
192    Returns a compiled code object.
193    mode = 'exec' or 'eval'
194    """
195
196    # python2 needs filename encoded
197    if not cpy3:
198        filename = filename.encode('utf-8')
199
200    try:
201        tree = ast.parse(code, filename, mode)
202    except Exception as e:
203        raise ValueError(_('Unable to parse file: %s') % cstr(e))
204
205    if not ignoresecurity:
206        visitor = CheckNodeVisitor()
207        visitor.visit(tree)
208
209    compiled = compile(tree, filename, mode)
210
211    return compiled
212