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