def grade(self, grader_path, grader_config, student_response): tests = [] errors = [] correct = 0 score = 0 if grader_path.endswith('/correct'): correct = 1 score = 1 tests.append(('short', 'long', True, 'expected', 'actual')) tests.append(('short', '', True, 'expected', 'actual')) elif grader_path.endswith('/incorrect'): tests.append(('short', 'long', False, 'expected', 'actual')) errors.append('THIS IS AN ERROR') errors.append('\x00\xc3\x83\xc3\xb8\x02') try: from codejail import jail_code except ImportError: tests.append(("codejail", "codejail not installed", True, "", "")) else: if jail_code.is_configured("python"): tests.append(("codejail", "codejail configured", True, "", "")) else: tests.append( ("codejail", "codejail not configured", True, "", "")) results = { 'correct': correct, 'score': score, 'tests': tests, 'errors': errors, } return results
def run(command, code, data=None, files=None, output_limit=None, **kwargs): assert jail_code.is_configured('python') files = files or [] files.append((code, 'quiz.py')) if data is not None: data = utils.encode(data) runcode = inspect.getsource(stepicrun) argv = ['--command={0}'.format(command), '--code-path=quiz.py'] for k, v in kwargs.items(): argv.append('--{0}={1}'.format(k, v)) result = jail_code_wrapper("python", code=runcode, argv=argv, files=files, stdin=data) if result.status != 0: stderr = result.stderr.decode() if result.time_limit_exceeded: stderr = "Time limit exceeded\n" + stderr msg = "{} failed:\n{}".format(command, stderr) raise JailedCodeFailed(msg) if output_limit and result.stdout and len(result.stdout) > output_limit: raise JailedCodeFailed("output is too large") try: decoded = utils.decode(result.stdout) if result.stdout else None except ValueError: raise JailedCodeFailed("failed to decode edyrun stdout:\n{}".format( result.stdout)) return decoded
def run(command, code, data=None, files=None, output_limit=None, **kwargs): assert jail_code.is_configured("python") files = files or [] files.append((code, "quiz.py")) if data is not None: data = utils.encode(data) runcode = inspect.getsource(stepicrun) argv = ["--command={0}".format(command), "--code-path=quiz.py"] for k, v in kwargs.items(): argv.append("--{0}={1}".format(k, v)) result = jail_code_wrapper("python", code=runcode, argv=argv, files=files, stdin=data) if result.status != 0: stderr = result.stderr.decode() if result.time_limit_exceeded: stderr = "Time limit exceeded\n" + stderr msg = "{} failed:\n{}".format(command, stderr) raise JailedCodeFailed(msg) if output_limit and result.stdout and len(result.stdout) > output_limit: raise JailedCodeFailed("output is too large") try: decoded = utils.decode(result.stdout) if result.stdout else None except ValueError: raise JailedCodeFailed("failed to decode edyrun stdout:\n{}".format(result.stdout)) return decoded
def test_cant_do_something_forbidden(self): # Can't test for forbiddenness if CodeJail isn't configured for python. if not is_configured("python"): pytest.skip() g = {} with self.assertRaises(SafeExecException) as cm: safe_exec("import os; files = os.listdir('/')", g) assert "OSError" in text_type(cm.exception) assert "Permission denied" in text_type(cm.exception)
def test_cant_do_something_forbidden(self): # Can't test for forbiddenness if CodeJail isn't configured for python. if not is_configured("python"): raise SkipTest g = {} with self.assertRaises(SafeExecException) as cm: safe_exec("import os; files = os.listdir('/')", g) self.assertIn("OSError", cm.exception.message) self.assertIn("Permission denied", cm.exception.message)
def test_cant_do_something_forbidden(self): ''' Demonstrates that running unsafe code inside the code jail throws SafeExecException, protecting the calling process. ''' # Can't test for forbiddenness if CodeJail isn't configured for python. if not jail_code.is_configured("python"): pytest.skip() g = {} with pytest.raises(SafeExecException) as cm: safe_exec('import sys; sys.exit(1)', g) assert "SystemExit" not in text_type(cm) assert "Couldn't execute jailed code" in text_type(cm)
g_dict = json_safe(globals_dict) with temp_directory() 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 except Exception as e: # Wrap the exception in a SafeExecException, but we don't # try here to include the traceback, since this is just a # substitute implementation. msg = "{0.__class__.__name__}: {0!s}".format(e) raise SafeExecException(msg) finally: sys.path = original_path globals_dict.update(json_safe(g_dict)) # Running Python code in the sandbox makes it difficult to debug. NO_SAFE_PYTHON = not jail_code.is_configured("python") if ALWAYS_BE_UNSAFE or NO_SAFE_PYTHON: # pragma: no cover safe_exec = not_safe_exec
def setUp(self): super(JailCodeHelpers, self).setUp() if not is_configured("python"): raise SkipTest
def jail_code(command, code=None, files=None, extra_files=None, argv=None, stdin=None, slug=None): """ Run code in a jailed subprocess. `command` is an abstract command ("python", "node", ...) that must have been configured using `configure`. `code` is a string containing the code to run. If no code is supplied, then the code to run must be in one of the `files` copied, and must be named in the `argv` list. `files` is a list of file paths, they are all copied to the jailed directory. Note that no check is made here that the files don't contain sensitive information. The caller must somehow determine whether to allow the code access to the files. Symlinks will be copied as symlinks. If the linked-to file is not accessible to the sandbox, the symlink will be unreadable as well. `extra_files` is a list of pairs, each pair is a filename and a bytestring of contents to write into that file. These files will be created in the temp directory and cleaned up automatically. No subdirectories are supported in the filename. `argv` is the command-line arguments to supply. `stdin` is a string, the data to provide as the stdin for the process. `slug` is an arbitrary string, a description that's meaningful to the caller, that will be used in log messages. Return an object with: .stdout: stdout of the program, a string .stderr: stderr of the program, a string .status: exit status of the process: an int, 0 for success """ if not is_configured(command): raise Exception("jail_code needs to be configured for %r" % command) # We make a temp directory to serve as the home of the sandboxed code. # It has a writable "tmp" directory within it for temp files. with temp_directory() as homedir: # Make directory readable by other users ('sandbox' user needs to be # able to read it). os.chmod(homedir, 0775) # Make a subdir to use for temp files, world-writable so that the # sandbox user can write to it. tmptmp = os.path.join(homedir, "tmp") os.mkdir(tmptmp) os.chmod(tmptmp, 0777) argv = argv or [] # All the supporting files are copied into our directory. for filename in files or (): dest = os.path.join(homedir, os.path.basename(filename)) if os.path.islink(filename): os.symlink(os.readlink(filename), dest) elif os.path.isfile(filename): shutil.copy(filename, homedir) else: shutil.copytree(filename, dest, symlinks=True) # Create the main file. if code: with open(os.path.join(homedir, "jailed_code"), "wb") as jailed: jailed.write(code) argv = ["jailed_code"] + argv # Create extra files requested by the caller: for name, content in extra_files or (): with open(os.path.join(homedir, name), "wb") as extra: extra.write(content) cmd = [] # Build the command to run. user = COMMANDS[command]['user'] if user: # Run as the specified user cmd.extend(['sudo', '-u', user]) # Point TMPDIR at our temp directory. cmd.extend(['TMPDIR=tmp']) # Start with the command line dictated by "python" or whatever. cmd.extend(COMMANDS[command]['cmdline_start']) # Add the code-specific command line pieces. cmd.extend(argv) # Run the subprocess. subproc = subprocess.Popen( cmd, preexec_fn=set_process_limits, cwd=homedir, env={}, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) if slug: log.info("Executing jailed code %s in %s, with PID %s", slug, homedir, subproc.pid) # Start the time killer thread. realtime = LIMITS["REALTIME"] if realtime: killer = ProcessKillerThread(subproc, limit=realtime) killer.start() result = JailResult() result.stdout, result.stderr = subproc.communicate(stdin) result.status = subproc.returncode return result
exec code in g_dict except Exception as e: # Wrap the exception in a SafeExecException, but we don't # try here to include the traceback, since this is just a # substitute implementation. msg = "{0.__class__.__name__}: {0!s}".format(e) raise SafeExecException(msg) finally: sys.path = original_path globals_dict.update(json_safe(g_dict)) # If the developer wants us to be unsafe (ALWAYS_BE_UNSAFE), or if there isn't # a configured jail for Python, then we'll be UNSAFE. UNSAFE = ALWAYS_BE_UNSAFE or not jail_code.is_configured("python") if UNSAFE: # pragma: no cover # Make safe_exec actually call not_safe_exec, but log that we're doing so. def safe_exec(*args, **kwargs): # pylint: disable=E0102 """An actually-unsafe safe_exec, that warns it's being used.""" # Because it would be bad if this function were used in production, # let's log a warning when it is used. Developers can live with # one more log line. slug = kwargs.get('slug', None) log.warning("Using codejail/safe_exec.py:not_safe_exec for %s", slug) return not_safe_exec(*args, **kwargs)
def setUp(self): super().setUp() if not is_configured("python"): raise SkipTest
def assert_configured(cmd): self.assertTrue(jail_code.is_configured(cmd), cmd + " is not configured")