Commit a04317b3 by Ned Batchelder

Files are properly copied in both implementations of safe_exec, and a new…

Files are properly copied in both implementations of safe_exec, and a new python_path argument adds to the python path.
parent abb91745
...@@ -454,9 +454,8 @@ class LoncapaProblem(object): ...@@ -454,9 +454,8 @@ class LoncapaProblem(object):
python_path.extend(self._extract_system_path(script)) python_path.extend(self._extract_system_path(script))
code = script.text
XMLESC = {"'": "'", """: '"'} XMLESC = {"'": "'", """: '"'}
code = unescape(code, XMLESC) code = unescape(script.text, XMLESC)
all_code += code all_code += code
if all_code: if all_code:
......
...@@ -2,16 +2,23 @@ ...@@ -2,16 +2,23 @@
import codejail.safe_exec import codejail.safe_exec
# This will set up the name "random" as a properly-seeded stand-in for the
# random module.
CODE_PROLOG = """\ CODE_PROLOG = """\
import random as random_module import random as random_module
random = random_module.Random(%r) random = random_module.Random(%r)
random.Random = random_module.Random 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 code_prolog = CODE_PROLOG % random_seed
codejail.safe_exec.safe_exec( codejail.safe_exec.safe_exec(
code_prolog + code, globals_dict, locals_dict, future_division=True, code_prolog + code, globals_dict, locals_dict, future_division=True,
python_path=python_path,
assumed_imports=[ assumed_imports=[
"numpy", "numpy",
"math", "math",
......
"""Test safe_exec.py""" """Test safe_exec.py"""
import os.path
import random import random
import unittest import unittest
...@@ -35,3 +36,11 @@ class TestSafeExec(unittest.TestCase): ...@@ -35,3 +36,11 @@ class TestSafeExec(unittest.TestCase):
# With a seed, the results are predictable # With a seed, the results are predictable
safe_exec("rnums = [random.randint(0, 999) for _ in xrange(100)]", g, l, random_seed=17) safe_exec("rnums = [random.randint(0, 999) for _ in xrange(100)]", g, l, random_seed=17)
self.assertEqual(l['rnums'], rnums) 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]
)
...@@ -63,8 +63,11 @@ def jailpy(code, files=None, argv=None, stdin=None): ...@@ -63,8 +63,11 @@ def jailpy(code, files=None, argv=None, stdin=None):
# All the supporting files are copied into our directory. # All the supporting files are copied into our directory.
for filename in files or (): for filename in files or ():
dest = os.path.join(tmpdir, os.path.basename(filename)) if os.path.isfile(filename):
shutil.copyfile(filename, dest) shutil.copy(filename, tmpdir)
else:
dest = os.path.join(tmpdir, os.path.basename(filename))
shutil.copytree(filename, dest)
# Create the main file. # Create the main file.
with open(os.path.join(tmpdir, "jailed_code.py"), "w") as jailed: with open(os.path.join(tmpdir, "jailed_code.py"), "w") as jailed:
......
"""Safe execution of untrusted Python code.""" """Safe execution of untrusted Python code."""
import json import json
import os.path
import shutil
import sys
import textwrap import textwrap
import lazymod import lazymod
import jailpy 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. # We'll need the code from lazymod.py for use in jailpy, so read it now.
lazymod_py_file = lazymod.__file__ lazymod_py_file = lazymod.__file__
if lazymod_py_file.endswith("c"): if lazymod_py_file.endswith("c"):
...@@ -23,7 +28,7 @@ def names_and_modules(assumed_imports): ...@@ -23,7 +28,7 @@ def names_and_modules(assumed_imports):
yield modname, modname 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. """Execute code as "exec" does, but safely.
`code` is a string of Python code. `globals_dict` and `locals_dict` are `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 ...@@ -41,6 +46,7 @@ def safe_exec(code, globals_dict, locals_dict, future_division=False, assumed_im
""" """
the_code = [] the_code = []
files = list(files or ())
if future_division: if future_division:
the_code.append("from __future__ import division\n") 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 ...@@ -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) 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: if assumed_imports:
the_code.append(lazymod_py) the_code.append(lazymod_py)
for name, modname in names_and_modules(assumed_imports): 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 ...@@ -63,25 +74,30 @@ def safe_exec(code, globals_dict, locals_dict, future_division=False, assumed_im
json.dump(l_dict, sys.stdout) 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. # Turn this on to see what's being executed.
if 0: if 1:
print "--{:-<40}".format(" jailed ") print "--{:-<40}".format(" jailed ")
print "".join(the_code) print jailed_code
print "--{:-<40}".format(" exec ") print "--{:-<40}".format(" exec ")
print code print code
stdin = json.dumps([code, globals_dict, locals_dict]) res = jailpy.jailpy(jailed_code, stdin=stdin, files=files)
res = jailpy.jailpy("".join(the_code), stdin=stdin)
if res.status != 0: if res.status != 0:
raise Exception("Couldn't excecute jailed code: %s" % res.stderr) raise Exception("Couldn't excecute jailed code: %s" % res.stderr)
locals_dict.update(json.loads(res.stdout)) 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. """Another implementation of `safe_exec`, but not safe.
This can be swapped in for debugging problems in sandboxed Python code. 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): def straw(d):
"""Return only the JSON-safe part of 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 ...@@ -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 ()): for name, modname in names_and_modules(assumed_imports or ()):
g_dict[name] = lazymod.LazyModule(modname) 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)) locals_dict.update(straw(l_dict))
......
"""Test jailpy.py""" """Test jailpy.py"""
import os.path
import textwrap import textwrap
import unittest import unittest
from nose.plugins.skip import SkipTest from nose.plugins.skip import SkipTest
...@@ -53,6 +54,14 @@ class TestFeatures(JailPyHelpers, unittest.TestCase): ...@@ -53,6 +54,14 @@ class TestFeatures(JailPyHelpers, unittest.TestCase):
self.assertResultOk(res) self.assertResultOk(res)
self.assertEqual(res.stdout.strip(), "36.5") 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): class TestLimits(JailPyHelpers, unittest.TestCase):
def test_cant_use_too_much_memory(self): def test_cant_use_too_much_memory(self):
......
"""Test safe_exec.py""" """Test safe_exec.py"""
import textwrap import os.path
import unittest import unittest
from nose.plugins.skip import SkipTest from nose.plugins.skip import SkipTest
from codejail.safe_exec import safe_exec, not_safe_exec from codejail.safe_exec import safe_exec, not_safe_exec
dedent = textwrap.dedent
class SafeExecTests(object): class SafeExecTests(object):
"""The tests for `safe_exec`, will be mixed into specific test classes below.""" """The tests for `safe_exec`, will be mixed into specific test classes below."""
def test_set_values(self): def test_set_values(self):
...@@ -37,6 +35,22 @@ class SafeExecTests(object): ...@@ -37,6 +35,22 @@ class SafeExecTests(object):
self.assertEqual(l['a'][0], 'x') self.assertEqual(l['a'][0], 'x')
self.assertEqual(l['a'][-1], 'y') 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): class TestSafeExec(SafeExecTests, unittest.TestCase):
"""Run SafeExecTests, with the real safe_exec.""" """Run SafeExecTests, with the real safe_exec."""
......
...@@ -49,3 +49,23 @@ class ModuleIsolation(object): ...@@ -49,3 +49,23 @@ class ModuleIsolation(object):
# and delete them all so another import will run code for real again. # and delete them all so another import will run code for real again.
for m in new_mods: for m in new_mods:
del sys.modules[m] 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()
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment