• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1"""Restricted execution facilities.
2
3The class RExec exports methods r_exec(), r_eval(), r_execfile(), and
4r_import(), which correspond roughly to the built-in operations
5exec, eval(), execfile() and import, but executing the code in an
6environment that only exposes those built-in operations that are
7deemed safe.  To this end, a modest collection of 'fake' modules is
8created which mimics the standard modules by the same names.  It is a
9policy decision which built-in modules and operations are made
10available; this module provides a reasonable default, but derived
11classes can change the policies e.g. by overriding or extending class
12variables like ok_builtin_modules or methods like make_sys().
13
14XXX To do:
15- r_open should allow writing tmp dir
16- r_exec etc. with explicit globals/locals? (Use rexec("exec ... in ...")?)
17
18"""
19from warnings import warnpy3k
20warnpy3k("the rexec module has been removed in Python 3.0", stacklevel=2)
21del warnpy3k
22
23
24import sys
25import __builtin__
26import os
27import ihooks
28import imp
29
30__all__ = ["RExec"]
31
32class FileBase:
33
34    ok_file_methods = ('fileno', 'flush', 'isatty', 'read', 'readline',
35            'readlines', 'seek', 'tell', 'write', 'writelines', 'xreadlines',
36            '__iter__')
37
38
39class FileWrapper(FileBase):
40
41    # XXX This is just like a Bastion -- should use that!
42
43    def __init__(self, f):
44        for m in self.ok_file_methods:
45            if not hasattr(self, m) and hasattr(f, m):
46                setattr(self, m, getattr(f, m))
47
48    def close(self):
49        self.flush()
50
51
52TEMPLATE = """
53def %s(self, *args):
54        return getattr(self.mod, self.name).%s(*args)
55"""
56
57class FileDelegate(FileBase):
58
59    def __init__(self, mod, name):
60        self.mod = mod
61        self.name = name
62
63    for m in FileBase.ok_file_methods + ('close',):
64        exec TEMPLATE % (m, m)
65
66
67class RHooks(ihooks.Hooks):
68
69    def __init__(self, *args):
70        # Hacks to support both old and new interfaces:
71        # old interface was RHooks(rexec[, verbose])
72        # new interface is RHooks([verbose])
73        verbose = 0
74        rexec = None
75        if args and type(args[-1]) == type(0):
76            verbose = args[-1]
77            args = args[:-1]
78        if args and hasattr(args[0], '__class__'):
79            rexec = args[0]
80            args = args[1:]
81        if args:
82            raise TypeError, "too many arguments"
83        ihooks.Hooks.__init__(self, verbose)
84        self.rexec = rexec
85
86    def set_rexec(self, rexec):
87        # Called by RExec instance to complete initialization
88        self.rexec = rexec
89
90    def get_suffixes(self):
91        return self.rexec.get_suffixes()
92
93    def is_builtin(self, name):
94        return self.rexec.is_builtin(name)
95
96    def init_builtin(self, name):
97        m = __import__(name)
98        return self.rexec.copy_except(m, ())
99
100    def init_frozen(self, name): raise SystemError, "don't use this"
101    def load_source(self, *args): raise SystemError, "don't use this"
102    def load_compiled(self, *args): raise SystemError, "don't use this"
103    def load_package(self, *args): raise SystemError, "don't use this"
104
105    def load_dynamic(self, name, filename, file):
106        return self.rexec.load_dynamic(name, filename, file)
107
108    def add_module(self, name):
109        return self.rexec.add_module(name)
110
111    def modules_dict(self):
112        return self.rexec.modules
113
114    def default_path(self):
115        return self.rexec.modules['sys'].path
116
117
118# XXX Backwards compatibility
119RModuleLoader = ihooks.FancyModuleLoader
120RModuleImporter = ihooks.ModuleImporter
121
122
123class RExec(ihooks._Verbose):
124    """Basic restricted execution framework.
125
126    Code executed in this restricted environment will only have access to
127    modules and functions that are deemed safe; you can subclass RExec to
128    add or remove capabilities as desired.
129
130    The RExec class can prevent code from performing unsafe operations like
131    reading or writing disk files, or using TCP/IP sockets.  However, it does
132    not protect against code using extremely large amounts of memory or
133    processor time.
134
135    """
136
137    ok_path = tuple(sys.path)           # That's a policy decision
138
139    ok_builtin_modules = ('audioop', 'array', 'binascii',
140                          'cmath', 'errno', 'imageop',
141                          'marshal', 'math', 'md5', 'operator',
142                          'parser', 'select',
143                          'sha', '_sre', 'strop', 'struct', 'time',
144                          '_weakref')
145
146    ok_posix_names = ('error', 'fstat', 'listdir', 'lstat', 'readlink',
147                      'stat', 'times', 'uname', 'getpid', 'getppid',
148                      'getcwd', 'getuid', 'getgid', 'geteuid', 'getegid')
149
150    ok_sys_names = ('byteorder', 'copyright', 'exit', 'getdefaultencoding',
151                    'getrefcount', 'hexversion', 'maxint', 'maxunicode',
152                    'platform', 'ps1', 'ps2', 'version', 'version_info')
153
154    nok_builtin_names = ('open', 'file', 'reload', '__import__')
155
156    ok_file_types = (imp.C_EXTENSION, imp.PY_SOURCE)
157
158    def __init__(self, hooks = None, verbose = 0):
159        """Returns an instance of the RExec class.
160
161        The hooks parameter is an instance of the RHooks class or a subclass
162        of it.  If it is omitted or None, the default RHooks class is
163        instantiated.
164
165        Whenever the RExec module searches for a module (even a built-in one)
166        or reads a module's code, it doesn't actually go out to the file
167        system itself.  Rather, it calls methods of an RHooks instance that
168        was passed to or created by its constructor.  (Actually, the RExec
169        object doesn't make these calls --- they are made by a module loader
170        object that's part of the RExec object.  This allows another level of
171        flexibility, which can be useful when changing the mechanics of
172        import within the restricted environment.)
173
174        By providing an alternate RHooks object, we can control the file
175        system accesses made to import a module, without changing the
176        actual algorithm that controls the order in which those accesses are
177        made.  For instance, we could substitute an RHooks object that
178        passes all filesystem requests to a file server elsewhere, via some
179        RPC mechanism such as ILU.  Grail's applet loader uses this to support
180        importing applets from a URL for a directory.
181
182        If the verbose parameter is true, additional debugging output may be
183        sent to standard output.
184
185        """
186
187        raise RuntimeError, "This code is not secure in Python 2.2 and later"
188
189        ihooks._Verbose.__init__(self, verbose)
190        # XXX There's a circular reference here:
191        self.hooks = hooks or RHooks(verbose)
192        self.hooks.set_rexec(self)
193        self.modules = {}
194        self.ok_dynamic_modules = self.ok_builtin_modules
195        list = []
196        for mname in self.ok_builtin_modules:
197            if mname in sys.builtin_module_names:
198                list.append(mname)
199        self.ok_builtin_modules = tuple(list)
200        self.set_trusted_path()
201        self.make_builtin()
202        self.make_initial_modules()
203        # make_sys must be last because it adds the already created
204        # modules to its builtin_module_names
205        self.make_sys()
206        self.loader = RModuleLoader(self.hooks, verbose)
207        self.importer = RModuleImporter(self.loader, verbose)
208
209    def set_trusted_path(self):
210        # Set the path from which dynamic modules may be loaded.
211        # Those dynamic modules must also occur in ok_builtin_modules
212        self.trusted_path = filter(os.path.isabs, sys.path)
213
214    def load_dynamic(self, name, filename, file):
215        if name not in self.ok_dynamic_modules:
216            raise ImportError, "untrusted dynamic module: %s" % name
217        if name in sys.modules:
218            src = sys.modules[name]
219        else:
220            src = imp.load_dynamic(name, filename, file)
221        dst = self.copy_except(src, [])
222        return dst
223
224    def make_initial_modules(self):
225        self.make_main()
226        self.make_osname()
227
228    # Helpers for RHooks
229
230    def get_suffixes(self):
231        return [item   # (suff, mode, type)
232                for item in imp.get_suffixes()
233                if item[2] in self.ok_file_types]
234
235    def is_builtin(self, mname):
236        return mname in self.ok_builtin_modules
237
238    # The make_* methods create specific built-in modules
239
240    def make_builtin(self):
241        m = self.copy_except(__builtin__, self.nok_builtin_names)
242        m.__import__ = self.r_import
243        m.reload = self.r_reload
244        m.open = m.file = self.r_open
245
246    def make_main(self):
247        self.add_module('__main__')
248
249    def make_osname(self):
250        osname = os.name
251        src = __import__(osname)
252        dst = self.copy_only(src, self.ok_posix_names)
253        dst.environ = e = {}
254        for key, value in os.environ.items():
255            e[key] = value
256
257    def make_sys(self):
258        m = self.copy_only(sys, self.ok_sys_names)
259        m.modules = self.modules
260        m.argv = ['RESTRICTED']
261        m.path = map(None, self.ok_path)
262        m.exc_info = self.r_exc_info
263        m = self.modules['sys']
264        l = self.modules.keys() + list(self.ok_builtin_modules)
265        l.sort()
266        m.builtin_module_names = tuple(l)
267
268    # The copy_* methods copy existing modules with some changes
269
270    def copy_except(self, src, exceptions):
271        dst = self.copy_none(src)
272        for name in dir(src):
273            setattr(dst, name, getattr(src, name))
274        for name in exceptions:
275            try:
276                delattr(dst, name)
277            except AttributeError:
278                pass
279        return dst
280
281    def copy_only(self, src, names):
282        dst = self.copy_none(src)
283        for name in names:
284            try:
285                value = getattr(src, name)
286            except AttributeError:
287                continue
288            setattr(dst, name, value)
289        return dst
290
291    def copy_none(self, src):
292        m = self.add_module(src.__name__)
293        m.__doc__ = src.__doc__
294        return m
295
296    # Add a module -- return an existing module or create one
297
298    def add_module(self, mname):
299        m = self.modules.get(mname)
300        if m is None:
301            self.modules[mname] = m = self.hooks.new_module(mname)
302        m.__builtins__ = self.modules['__builtin__']
303        return m
304
305    # The r* methods are public interfaces
306
307    def r_exec(self, code):
308        """Execute code within a restricted environment.
309
310        The code parameter must either be a string containing one or more
311        lines of Python code, or a compiled code object, which will be
312        executed in the restricted environment's __main__ module.
313
314        """
315        m = self.add_module('__main__')
316        exec code in m.__dict__
317
318    def r_eval(self, code):
319        """Evaluate code within a restricted environment.
320
321        The code parameter must either be a string containing a Python
322        expression, or a compiled code object, which will be evaluated in
323        the restricted environment's __main__ module.  The value of the
324        expression or code object will be returned.
325
326        """
327        m = self.add_module('__main__')
328        return eval(code, m.__dict__)
329
330    def r_execfile(self, file):
331        """Execute the Python code in the file in the restricted
332        environment's __main__ module.
333
334        """
335        m = self.add_module('__main__')
336        execfile(file, m.__dict__)
337
338    def r_import(self, mname, globals={}, locals={}, fromlist=[]):
339        """Import a module, raising an ImportError exception if the module
340        is considered unsafe.
341
342        This method is implicitly called by code executing in the
343        restricted environment.  Overriding this method in a subclass is
344        used to change the policies enforced by a restricted environment.
345
346        """
347        return self.importer.import_module(mname, globals, locals, fromlist)
348
349    def r_reload(self, m):
350        """Reload the module object, re-parsing and re-initializing it.
351
352        This method is implicitly called by code executing in the
353        restricted environment.  Overriding this method in a subclass is
354        used to change the policies enforced by a restricted environment.
355
356        """
357        return self.importer.reload(m)
358
359    def r_unload(self, m):
360        """Unload the module.
361
362        Removes it from the restricted environment's sys.modules dictionary.
363
364        This method is implicitly called by code executing in the
365        restricted environment.  Overriding this method in a subclass is
366        used to change the policies enforced by a restricted environment.
367
368        """
369        return self.importer.unload(m)
370
371    # The s_* methods are similar but also swap std{in,out,err}
372
373    def make_delegate_files(self):
374        s = self.modules['sys']
375        self.delegate_stdin = FileDelegate(s, 'stdin')
376        self.delegate_stdout = FileDelegate(s, 'stdout')
377        self.delegate_stderr = FileDelegate(s, 'stderr')
378        self.restricted_stdin = FileWrapper(sys.stdin)
379        self.restricted_stdout = FileWrapper(sys.stdout)
380        self.restricted_stderr = FileWrapper(sys.stderr)
381
382    def set_files(self):
383        if not hasattr(self, 'save_stdin'):
384            self.save_files()
385        if not hasattr(self, 'delegate_stdin'):
386            self.make_delegate_files()
387        s = self.modules['sys']
388        s.stdin = self.restricted_stdin
389        s.stdout = self.restricted_stdout
390        s.stderr = self.restricted_stderr
391        sys.stdin = self.delegate_stdin
392        sys.stdout = self.delegate_stdout
393        sys.stderr = self.delegate_stderr
394
395    def reset_files(self):
396        self.restore_files()
397        s = self.modules['sys']
398        self.restricted_stdin = s.stdin
399        self.restricted_stdout = s.stdout
400        self.restricted_stderr = s.stderr
401
402
403    def save_files(self):
404        self.save_stdin = sys.stdin
405        self.save_stdout = sys.stdout
406        self.save_stderr = sys.stderr
407
408    def restore_files(self):
409        sys.stdin = self.save_stdin
410        sys.stdout = self.save_stdout
411        sys.stderr = self.save_stderr
412
413    def s_apply(self, func, args=(), kw={}):
414        self.save_files()
415        try:
416            self.set_files()
417            r = func(*args, **kw)
418        finally:
419            self.restore_files()
420        return r
421
422    def s_exec(self, *args):
423        """Execute code within a restricted environment.
424
425        Similar to the r_exec() method, but the code will be granted access
426        to restricted versions of the standard I/O streams sys.stdin,
427        sys.stderr, and sys.stdout.
428
429        The code parameter must either be a string containing one or more
430        lines of Python code, or a compiled code object, which will be
431        executed in the restricted environment's __main__ module.
432
433        """
434        return self.s_apply(self.r_exec, args)
435
436    def s_eval(self, *args):
437        """Evaluate code within a restricted environment.
438
439        Similar to the r_eval() method, but the code will be granted access
440        to restricted versions of the standard I/O streams sys.stdin,
441        sys.stderr, and sys.stdout.
442
443        The code parameter must either be a string containing a Python
444        expression, or a compiled code object, which will be evaluated in
445        the restricted environment's __main__ module.  The value of the
446        expression or code object will be returned.
447
448        """
449        return self.s_apply(self.r_eval, args)
450
451    def s_execfile(self, *args):
452        """Execute the Python code in the file in the restricted
453        environment's __main__ module.
454
455        Similar to the r_execfile() method, but the code will be granted
456        access to restricted versions of the standard I/O streams sys.stdin,
457        sys.stderr, and sys.stdout.
458
459        """
460        return self.s_apply(self.r_execfile, args)
461
462    def s_import(self, *args):
463        """Import a module, raising an ImportError exception if the module
464        is considered unsafe.
465
466        This method is implicitly called by code executing in the
467        restricted environment.  Overriding this method in a subclass is
468        used to change the policies enforced by a restricted environment.
469
470        Similar to the r_import() method, but has access to restricted
471        versions of the standard I/O streams sys.stdin, sys.stderr, and
472        sys.stdout.
473
474        """
475        return self.s_apply(self.r_import, args)
476
477    def s_reload(self, *args):
478        """Reload the module object, re-parsing and re-initializing it.
479
480        This method is implicitly called by code executing in the
481        restricted environment.  Overriding this method in a subclass is
482        used to change the policies enforced by a restricted environment.
483
484        Similar to the r_reload() method, but has access to restricted
485        versions of the standard I/O streams sys.stdin, sys.stderr, and
486        sys.stdout.
487
488        """
489        return self.s_apply(self.r_reload, args)
490
491    def s_unload(self, *args):
492        """Unload the module.
493
494        Removes it from the restricted environment's sys.modules dictionary.
495
496        This method is implicitly called by code executing in the
497        restricted environment.  Overriding this method in a subclass is
498        used to change the policies enforced by a restricted environment.
499
500        Similar to the r_unload() method, but has access to restricted
501        versions of the standard I/O streams sys.stdin, sys.stderr, and
502        sys.stdout.
503
504        """
505        return self.s_apply(self.r_unload, args)
506
507    # Restricted open(...)
508
509    def r_open(self, file, mode='r', buf=-1):
510        """Method called when open() is called in the restricted environment.
511
512        The arguments are identical to those of the open() function, and a
513        file object (or a class instance compatible with file objects)
514        should be returned.  RExec's default behaviour is allow opening
515        any file for reading, but forbidding any attempt to write a file.
516
517        This method is implicitly called by code executing in the
518        restricted environment.  Overriding this method in a subclass is
519        used to change the policies enforced by a restricted environment.
520
521        """
522        mode = str(mode)
523        if mode not in ('r', 'rb'):
524            raise IOError, "can't open files for writing in restricted mode"
525        return open(file, mode, buf)
526
527    # Restricted version of sys.exc_info()
528
529    def r_exc_info(self):
530        ty, va, tr = sys.exc_info()
531        tr = None
532        return ty, va, tr
533
534
535def test():
536    import getopt, traceback
537    opts, args = getopt.getopt(sys.argv[1:], 'vt:')
538    verbose = 0
539    trusted = []
540    for o, a in opts:
541        if o == '-v':
542            verbose = verbose+1
543        if o == '-t':
544            trusted.append(a)
545    r = RExec(verbose=verbose)
546    if trusted:
547        r.ok_builtin_modules = r.ok_builtin_modules + tuple(trusted)
548    if args:
549        r.modules['sys'].argv = args
550        r.modules['sys'].path.insert(0, os.path.dirname(args[0]))
551    else:
552        r.modules['sys'].path.insert(0, "")
553    fp = sys.stdin
554    if args and args[0] != '-':
555        try:
556            fp = open(args[0])
557        except IOError, msg:
558            print "%s: can't open file %r" % (sys.argv[0], args[0])
559            return 1
560    if fp.isatty():
561        try:
562            import readline
563        except ImportError:
564            pass
565        import code
566        class RestrictedConsole(code.InteractiveConsole):
567            def runcode(self, co):
568                self.locals['__builtins__'] = r.modules['__builtin__']
569                r.s_apply(code.InteractiveConsole.runcode, (self, co))
570        try:
571            RestrictedConsole(r.modules['__main__'].__dict__).interact()
572        except SystemExit, n:
573            return n
574    else:
575        text = fp.read()
576        fp.close()
577        c = compile(text, fp.name, 'exec')
578        try:
579            r.s_exec(c)
580        except SystemExit, n:
581            return n
582        except:
583            traceback.print_exc()
584            return 1
585
586
587if __name__ == '__main__':
588    sys.exit(test())
589