Skip to content

Commit 68918ef

Browse files
committed
fix: Require an explicit opt in to unsafety; defer decision to call time
Codejail currently makes a decision at module load time of whether it should run all code safely or unsafely, and defaults to unsafely. This causes several problems: - Any misconfiguration of codejail (such as a missing Django setting or middleware) results in the application becoming immediately and entirely vulnerable to anyone who can submit code. - Codejail's behavior changes depending on when the `codejail.safe_exec` module is loaded during application initialization. This causes unstable behavior and is confusing for developers. This change switches the `ALWAYS_BE_UNSAFE` check to occur only at the time that `safe_exec` is actually called, rather than at module load time. The check for whether codejail is configured for Python is also moved to call time, but no longer automatically switches codejail to unsafe mode. Instead, it raises an exception to notify the user of their error.
1 parent 276c76d commit 68918ef

4 files changed

Lines changed: 70 additions & 50 deletions

File tree

README.rst

Lines changed: 13 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,11 @@ designed primarily for Python execution, but can be used for other languages as
66
well.
77

88
Security is enforced with AppArmor. If your operating system doesn't support
9-
AppArmor, then CodeJail won't protect the execution.
9+
AppArmor, or if the AppArmor profile is not defined and configured correctly,
10+
then CodeJail will not protect the execution.
1011

1112
CodeJail is designed to be configurable, and will auto-configure itself for
12-
Python execution if you install it properly. The configuration is designed to
13-
be flexible: it can run in safe mode or unsafe mode. This helps support large
14-
development groups where only some of the developers are involved enough with
15-
secure execution to configure AppArmor on their development machines.
16-
17-
If CodeJail is not configured for safe execution, it will execution Python
18-
using the same API, but will not guard against malicious code. This allows the
19-
same code to be used on safe-configured or non-safe-configured developer's
20-
machines.
13+
Python execution if you install it properly.
2114

2215
A CodeJail sandbox consists of several pieces:
2316

@@ -66,10 +59,12 @@ Installation
6659
------------
6760

6861
These instructions detail how to configure your operating system so that
69-
CodeJail can execute Python code safely. You can run CodeJail without these
70-
steps, and you will have an unsafe CodeJail. This is fine for developers'
71-
machines who are unconcerned with security, and simplifies the integration of
72-
CodeJail into your project.
62+
CodeJail can execute Python code safely. However, it is also possible to set
63+
``codejail.safe_exec.ALWAYS_BE_UNSAFE = True`` and execute submitted Python
64+
directly on the machine, with no security whatsoever. This may be fine for
65+
developers' machines who are unconcerned with security, and allows testing
66+
an integration with CodeJail's API. It must not be used if any input is coming
67+
from untrusted sources, however. **Do not use this option in production systems.**
7368

7469
To secure Python execution, you'll be creating a new virtualenv. This means
7570
you'll have two: the main virtualenv for your project, and the new one for
@@ -212,8 +207,8 @@ the rights to modify the files in its site-packages directory.
212207
Tests
213208
-----
214209

215-
In order to target the sandboxed Python environment(s) you have created on your
216-
system, you must set the following environment variables for testing::
210+
To run tests, you must perform the standard installation steps. Then
211+
you must set the following environment variables::
217212

218213
$ export CODEJAIL_TEST_USER=<owner of sandbox (usually 'sandbox')>
219214
$ export CODEJAIL_TEST_VENV=<SANDENV>
@@ -222,10 +217,7 @@ Run the tests with the Makefile::
222217

223218
$ make tests
224219

225-
If CodeJail is running unsafely, many of the tests will be automatically
226-
skipped, or will fail, depending on whether CodeJail thinks it should be in
227-
safe mode or not.
228-
220+
Several proxy tests are skipped if proxy mode is not configured.
229221

230222
Design
231223
------
@@ -260,7 +252,7 @@ the subprocess as JSON.
260252
Limitations
261253
-----------
262254

263-
* If codejail or AppArmor is not configured properly, codejail will default to
255+
* If codejail or AppArmor is not configured properly, codejail may default to
264256
running code insecurely (no sandboxing). It is not secure by default.
265257
* Sandbox isolation is achieved via AppArmor confinement. Codejail facilitates
266258
this, but cannot isolate execution without the use of AppArmor.

codejail/jail_code.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ def is_configured(command):
6868
)
6969

7070
if running_in_virtualenv:
71-
# On jenkins
71+
# In test environment
7272
sandbox_user = os.getenv('CODEJAIL_TEST_USER')
7373
sandbox_env = os.getenv('CODEJAIL_TEST_VENV')
7474
if sandbox_env and sandbox_user:

codejail/safe_exec.py

Lines changed: 27 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,13 @@
2323

2424
# Set this to True to log all the code and globals being executed.
2525
LOG_ALL_CODE = False
26-
# Set this to True to use the unsafe code, so that you can debug it.
26+
27+
# Set this to True to run submitted code with no confinement and no sandbox.
28+
#
29+
# WARNING: This is deeply dangerous; anyone who can submit code can take
30+
# over the computer immediately and entirely.
31+
#
32+
# The only purpose of this setting is for local debugging.
2733
ALWAYS_BE_UNSAFE = False
2834

2935

@@ -80,8 +86,22 @@ def safe_exec(
8086
the code raises an exception, this function will raise `SafeExecException`
8187
with the stderr of the sandbox process, which usually includes the original
8288
exception message and traceback.
83-
8489
"""
90+
if ALWAYS_BE_UNSAFE:
91+
not_safe_exec(
92+
code,
93+
globals_dict,
94+
files=files,
95+
python_path=python_path,
96+
limit_overrides_context=limit_overrides_context,
97+
slug=slug,
98+
extra_files=extra_files,
99+
)
100+
return
101+
102+
if not jail_code.is_configured('python'):
103+
raise RuntimeError("safe_exec has not been configured for Python")
104+
85105
the_code = []
86106

87107
files = list(files or ())
@@ -257,6 +277,11 @@ def not_safe_exec(
257277
Note that `limit_overrides_context` is ignored here, because resource limits
258278
are not applied.
259279
"""
280+
# Because it would be bad if this function were used in production,
281+
# let's log a warning when it is used. Developers can live with
282+
# one more log line.
283+
log.warning("DANGER: Executing code with `not_safe_exec` for %s", slug)
284+
260285
g_dict = json_safe(globals_dict)
261286

262287
with temp_directory() as tmpdir:
@@ -286,22 +311,3 @@ def not_safe_exec(
286311
sys.path = original_path
287312

288313
globals_dict.update(json_safe(g_dict))
289-
290-
291-
# If the developer wants us to be unsafe (ALWAYS_BE_UNSAFE), or if there isn't
292-
# a configured jail for Python, then we'll be UNSAFE.
293-
UNSAFE = ALWAYS_BE_UNSAFE or not jail_code.is_configured("python")
294-
295-
if UNSAFE: # pragma: no cover
296-
# Make safe_exec actually call not_safe_exec, but log that we're doing so.
297-
298-
def safe_exec(*args, **kwargs): # pylint: disable=E0102
299-
"""An actually-unsafe safe_exec, that warns it's being used."""
300-
301-
# Because it would be bad if this function were used in production,
302-
# let's log a warning when it is used. Developers can live with
303-
# one more log line.
304-
slug = kwargs.get('slug', None)
305-
log.warning("Using codejail/safe_exec.py:not_safe_exec for %s", slug)
306-
307-
return not_safe_exec(*args, **kwargs)

codejail/tests/test_safe_exec.py

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@
44
import textwrap
55
import zipfile
66
from io import BytesIO
7-
from unittest import SkipTest, TestCase
7+
from unittest import TestCase
8+
from unittest.mock import patch
9+
10+
import pytest
811

912
from codejail import safe_exec
1013
from codejail.jail_code import set_limit
@@ -176,17 +179,36 @@ class TestSafeExec(SafeExecTests, TestCase):
176179
def safe_exec(self, *args, **kwargs):
177180
safe_exec.safe_exec(*args, **kwargs)
178181

182+
@patch('codejail.jail_code.is_configured', return_value=False)
183+
def test_configuration(self, mock_is_configured):
184+
"""
185+
When not configured for Python, raise an exception.
186+
"""
187+
with pytest.raises(RuntimeError, match=r'safe_exec has not been configured for Python'):
188+
self.safe_exec('out = 1 + 2', {})
189+
190+
mock_is_configured.assert_called_once_with('python')
191+
192+
@patch('codejail.safe_exec.not_safe_exec')
193+
@patch('codejail.jail_code.is_configured', return_value=True)
194+
def test_opt_unsafe(self, mock_is_configured, mock_not_safe_exec):
195+
"""
196+
When ALWAYS_BE_UNSAFE enabled, go immediately to not_safe_exec.
197+
"""
198+
with patch.object(safe_exec, 'ALWAYS_BE_UNSAFE', new=True):
199+
self.safe_exec('out = 1 + 2', {})
200+
201+
mock_is_configured.assert_not_called()
202+
mock_not_safe_exec.assert_called_once_with(
203+
'out = 1 + 2', {},
204+
files=None, python_path=None, limit_overrides_context=None, slug=None, extra_files=None,
205+
)
206+
179207

180208
class TestNotSafeExec(SafeExecTests, TestCase):
181209
"""Run SafeExecTests, with not_safe_exec."""
182210

183211
__test__ = True
184212

185-
def setUp(self):
186-
# If safe_exec is actually an alias to not_safe_exec, then there's no
187-
# point running these tests.
188-
if safe_exec.UNSAFE: # pragma: no cover
189-
raise SkipTest
190-
191213
def safe_exec(self, *args, **kwargs):
192214
safe_exec.not_safe_exec(*args, **kwargs)

0 commit comments

Comments
 (0)