diff --git a/common/lib/capa/capa/capa_problem.py b/common/lib/capa/capa/capa_problem.py index 2e23d38380..b8e146d773 100644 --- a/common/lib/capa/capa/capa_problem.py +++ b/common/lib/capa/capa/capa_problem.py @@ -454,9 +454,8 @@ class LoncapaProblem(object): python_path.extend(self._extract_system_path(script)) - code = script.text XMLESC = {"'": "'", """: '"'} - code = unescape(code, XMLESC) + code = unescape(script.text, XMLESC) all_code += code if all_code: diff --git a/common/lib/capa/capa/safe_exec.py b/common/lib/capa/capa/safe_exec.py index 32f6049453..e10da30196 100644 --- a/common/lib/capa/capa/safe_exec.py +++ b/common/lib/capa/capa/safe_exec.py @@ -2,16 +2,23 @@ import codejail.safe_exec +# This will set up the name "random" as a properly-seeded stand-in for the +# random module. CODE_PROLOG = """\ import random as random_module random = random_module.Random(%r) random.Random = random_module.Random +del random_module """ -def safe_exec(code, globals_dict, locals_dict, random_seed=None): +def safe_exec(code, globals_dict, locals_dict, random_seed=None, python_path=None): + """Exec python code safely. + + """ code_prolog = CODE_PROLOG % random_seed codejail.safe_exec.safe_exec( code_prolog + code, globals_dict, locals_dict, future_division=True, + python_path=python_path, assumed_imports=[ "numpy", "math", diff --git a/common/lib/capa/capa/tests/test_files/pylib/constant.py b/common/lib/capa/capa/tests/test_files/pylib/constant.py new file mode 100644 index 0000000000..0769d528ba --- /dev/null +++ b/common/lib/capa/capa/tests/test_files/pylib/constant.py @@ -0,0 +1 @@ +THE_CONST = 23 diff --git a/common/lib/capa/capa/tests/test_safe_exec.py b/common/lib/capa/capa/tests/test_safe_exec.py index 035b15b24d..3a436ac3ce 100644 --- a/common/lib/capa/capa/tests/test_safe_exec.py +++ b/common/lib/capa/capa/tests/test_safe_exec.py @@ -1,5 +1,6 @@ """Test safe_exec.py""" +import os.path import random import unittest @@ -35,3 +36,11 @@ class TestSafeExec(unittest.TestCase): # With a seed, the results are predictable safe_exec("rnums = [random.randint(0, 999) for _ in xrange(100)]", g, l, random_seed=17) self.assertEqual(l['rnums'], rnums) + + def test_python_lib(self): + pylib = os.path.dirname(__file__) + "/test_files/pylib" + g, l = {}, {} + safe_exec( + "import constant; a = constant.THE_CONST", + g, l, python_path=[pylib] + ) diff --git a/common/lib/codejail/codejail/jailpy.py b/common/lib/codejail/codejail/jailpy.py index 8f68ab33de..ed13a0d3f0 100644 --- a/common/lib/codejail/codejail/jailpy.py +++ b/common/lib/codejail/codejail/jailpy.py @@ -63,8 +63,11 @@ def jailpy(code, files=None, argv=None, stdin=None): # All the supporting files are copied into our directory. for filename in files or (): - dest = os.path.join(tmpdir, os.path.basename(filename)) - shutil.copyfile(filename, dest) + if os.path.isfile(filename): + shutil.copy(filename, tmpdir) + else: + dest = os.path.join(tmpdir, os.path.basename(filename)) + shutil.copytree(filename, dest) # Create the main file. with open(os.path.join(tmpdir, "jailed_code.py"), "w") as jailed: diff --git a/common/lib/codejail/codejail/safe_exec.py b/common/lib/codejail/codejail/safe_exec.py index 04045fb782..06bc4c60d3 100644 --- a/common/lib/codejail/codejail/safe_exec.py +++ b/common/lib/codejail/codejail/safe_exec.py @@ -1,11 +1,16 @@ """Safe execution of untrusted Python code.""" import json +import os.path +import shutil +import sys import textwrap import lazymod import jailpy +from util import temp_directory, change_directory, TempDirectory + # We'll need the code from lazymod.py for use in jailpy, so read it now. lazymod_py_file = lazymod.__file__ if lazymod_py_file.endswith("c"): @@ -23,7 +28,7 @@ def names_and_modules(assumed_imports): yield modname, modname -def safe_exec(code, globals_dict, locals_dict, future_division=False, assumed_imports=None): +def safe_exec(code, globals_dict, locals_dict, future_division=False, assumed_imports=None, files=None, python_path=None): """Execute code as "exec" does, but safely. `code` is a string of Python code. `globals_dict` and `locals_dict` are @@ -41,6 +46,7 @@ def safe_exec(code, globals_dict, locals_dict, future_division=False, assumed_im """ the_code = [] + files = list(files or ()) if future_division: the_code.append("from __future__ import division\n") @@ -51,6 +57,11 @@ def safe_exec(code, globals_dict, locals_dict, future_division=False, assumed_im code, g_dict, l_dict = json.load(sys.stdin) """)) + for pydir in python_path or (): + pybase = os.path.basename(pydir) + the_code.append("sys.path.append(%r)\n" % pybase) + files.append(pydir) + if assumed_imports: the_code.append(lazymod_py) for name, modname in names_and_modules(assumed_imports): @@ -63,25 +74,30 @@ def safe_exec(code, globals_dict, locals_dict, future_division=False, assumed_im json.dump(l_dict, sys.stdout) """)) + stdin = json.dumps([code, globals_dict, locals_dict]) + jailed_code = "".join(the_code) + # Turn this on to see what's being executed. - if 0: + if 1: print "--{:-<40}".format(" jailed ") - print "".join(the_code) + print jailed_code print "--{:-<40}".format(" exec ") print code - stdin = json.dumps([code, globals_dict, locals_dict]) - res = jailpy.jailpy("".join(the_code), stdin=stdin) + res = jailpy.jailpy(jailed_code, stdin=stdin, files=files) if res.status != 0: raise Exception("Couldn't excecute jailed code: %s" % res.stderr) locals_dict.update(json.loads(res.stdout)) -def not_safe_exec(code, globals_dict, locals_dict, future_division=False, assumed_imports=None): +def not_safe_exec(code, globals_dict, locals_dict, future_division=False, assumed_imports=None, files=None, python_path=None): """Another implementation of `safe_exec`, but not safe. This can be swapped in for debugging problems in sandboxed Python code. + This is not thread-safe, due to temporarily changing the current directory + and modifying sys.path. + """ def straw(d): """Return only the JSON-safe part of d. @@ -108,7 +124,20 @@ def not_safe_exec(code, globals_dict, locals_dict, future_division=False, assume for name, modname in names_and_modules(assumed_imports or ()): g_dict[name] = lazymod.LazyModule(modname) - exec code in g_dict, l_dict + with temp_directory(delete_when_done=True) as tmpdir: + with change_directory(tmpdir): + # Copy the files here. + for filename in files or (): + dest = os.path.join(tmpdir, os.path.basename(filename)) + shutil.copyfile(filename, dest) + + original_path = sys.path + if python_path: + sys.path.extend(python_path) + try: + exec code in g_dict, l_dict + finally: + sys.path = original_path locals_dict.update(straw(l_dict)) diff --git a/common/lib/codejail/codejail/tests/hello.txt b/common/lib/codejail/codejail/tests/hello.txt new file mode 100644 index 0000000000..c12abce9f3 --- /dev/null +++ b/common/lib/codejail/codejail/tests/hello.txt @@ -0,0 +1 @@ +Hello there. diff --git a/common/lib/codejail/codejail/tests/pylib/module.py b/common/lib/codejail/codejail/tests/pylib/module.py new file mode 100644 index 0000000000..8cb5cded29 --- /dev/null +++ b/common/lib/codejail/codejail/tests/pylib/module.py @@ -0,0 +1 @@ +const = 42 diff --git a/common/lib/codejail/codejail/tests/test_jailpy.py b/common/lib/codejail/codejail/tests/test_jailpy.py index 3c6d13b714..fb59bac31d 100644 --- a/common/lib/codejail/codejail/tests/test_jailpy.py +++ b/common/lib/codejail/codejail/tests/test_jailpy.py @@ -1,5 +1,6 @@ """Test jailpy.py""" +import os.path import textwrap import unittest from nose.plugins.skip import SkipTest @@ -53,6 +54,14 @@ class TestFeatures(JailPyHelpers, unittest.TestCase): self.assertResultOk(res) self.assertEqual(res.stdout.strip(), "36.5") + def test_files_are_copied(self): + res = jailpy( + "print 'Look:', open('hello.txt').read()", + files=[os.path.dirname(__file__) + "/hello.txt"] + ) + self.assertResultOk(res) + self.assertEqual(res.stdout, 'Look: Hello there.\n\n') + class TestLimits(JailPyHelpers, unittest.TestCase): def test_cant_use_too_much_memory(self): diff --git a/common/lib/codejail/codejail/tests/test_safe_exec.py b/common/lib/codejail/codejail/tests/test_safe_exec.py index 685e2b8c39..00987f7787 100644 --- a/common/lib/codejail/codejail/tests/test_safe_exec.py +++ b/common/lib/codejail/codejail/tests/test_safe_exec.py @@ -1,13 +1,11 @@ """Test safe_exec.py""" -import textwrap +import os.path import unittest from nose.plugins.skip import SkipTest from codejail.safe_exec import safe_exec, not_safe_exec -dedent = textwrap.dedent - class SafeExecTests(object): """The tests for `safe_exec`, will be mixed into specific test classes below.""" def test_set_values(self): @@ -37,6 +35,22 @@ class SafeExecTests(object): self.assertEqual(l['a'][0], 'x') self.assertEqual(l['a'][-1], 'y') + def test_files_are_copied(self): + g, l = {}, {} + self.safe_exec( + "a = 'Look: ' + open('hello.txt').read()", g, l, + files=[os.path.dirname(__file__) + "/hello.txt"] + ) + self.assertEqual(l['a'], 'Look: Hello there.\n') + + def test_python_path(self): + g, l = {}, {} + self.safe_exec( + "import module; a = module.const", g, l, + python_path=[os.path.dirname(__file__) + "/pylib"] + ) + self.assertEqual(l['a'], 42) + class TestSafeExec(SafeExecTests, unittest.TestCase): """Run SafeExecTests, with the real safe_exec.""" diff --git a/common/lib/codejail/codejail/util.py b/common/lib/codejail/codejail/util.py index 88ecc1b319..e293ce052f 100644 --- a/common/lib/codejail/codejail/util.py +++ b/common/lib/codejail/codejail/util.py @@ -49,3 +49,23 @@ class ModuleIsolation(object): # and delete them all so another import will run code for real again. for m in new_mods: del sys.modules[m] + + +class ChangeDirectory(object): + def __init__(self, new_dir): + self.old_dir = os.getcwd() + os.chdir(new_dir) + + def clean_up(self): + os.chdir(self.old_dir) + +@contextlib.contextmanager +def change_directory(new_dir): + """ + A context manager to change the directory, and then change it back. + """ + cd = ChangeDirectory(new_dir) + try: + yield new_dir + finally: + cd.clean_up()