|
1 """Restricted execution facilities. |
|
2 |
|
3 The class RExec exports methods r_exec(), r_eval(), r_execfile(), and |
|
4 r_import(), which correspond roughly to the built-in operations |
|
5 exec, eval(), execfile() and import, but executing the code in an |
|
6 environment that only exposes those built-in operations that are |
|
7 deemed safe. To this end, a modest collection of 'fake' modules is |
|
8 created which mimics the standard modules by the same names. It is a |
|
9 policy decision which built-in modules and operations are made |
|
10 available; this module provides a reasonable default, but derived |
|
11 classes can change the policies e.g. by overriding or extending class |
|
12 variables like ok_builtin_modules or methods like make_sys(). |
|
13 |
|
14 XXX To do: |
|
15 - r_open should allow writing tmp dir |
|
16 - r_exec etc. with explicit globals/locals? (Use rexec("exec ... in ...")?) |
|
17 |
|
18 """ |
|
19 from warnings import warnpy3k |
|
20 warnpy3k("the rexec module has been removed in Python 3.0", stacklevel=2) |
|
21 del warnpy3k |
|
22 |
|
23 |
|
24 import sys |
|
25 import __builtin__ |
|
26 import os |
|
27 import ihooks |
|
28 import imp |
|
29 |
|
30 __all__ = ["RExec"] |
|
31 |
|
32 class FileBase: |
|
33 |
|
34 ok_file_methods = ('fileno', 'flush', 'isatty', 'read', 'readline', |
|
35 'readlines', 'seek', 'tell', 'write', 'writelines', 'xreadlines', |
|
36 '__iter__') |
|
37 |
|
38 |
|
39 class 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 |
|
52 TEMPLATE = """ |
|
53 def %s(self, *args): |
|
54 return getattr(self.mod, self.name).%s(*args) |
|
55 """ |
|
56 |
|
57 class 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 |
|
67 class 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 |
|
119 RModuleLoader = ihooks.FancyModuleLoader |
|
120 RModuleImporter = ihooks.ModuleImporter |
|
121 |
|
122 |
|
123 class 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 m = 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 |
|
535 def 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 |
|
587 if __name__ == '__main__': |
|
588 sys.exit(test()) |