Skip to content

Commit d6421dc

Browse files
authored
fix: Require an explicit opt in to unsafety; defer decision to call time (#246)
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 766be1d commit d6421dc

File tree

4 files changed

+70
-50
lines changed

4 files changed

+70
-50
lines changed

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

@@ -64,10 +57,12 @@ Installation
6457
------------
6558

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

7267
To secure Python execution, you'll be creating a new virtualenv. This means
7368
you'll have two: the main virtualenv for your project, and the new one for
@@ -166,8 +161,8 @@ the rights to modify the files in its site-packages directory.
166161
Tests
167162
-----
168163

169-
In order to target the sandboxed Python environment(s) you have created on your
170-
system, you must set the following environment variables for testing::
164+
To run tests, you must perform the standard installation steps. Then
165+
you must set the following environment variables::
171166

172167
$ export CODEJAIL_TEST_USER=<owner of sandbox (usually 'sandbox')>
173168
$ export CODEJAIL_TEST_VENV=<SANDENV>
@@ -176,10 +171,7 @@ Run the tests with the Makefile::
176171

177172
$ make tests
178173

179-
If CodeJail is running unsafely, many of the tests will be automatically
180-
skipped, or will fail, depending on whether CodeJail thinks it should be in
181-
safe mode or not.
182-
174+
Several proxy tests are skipped if proxy mode is not configured.
183175

184176
Design
185177
------
@@ -214,7 +206,7 @@ the subprocess as JSON.
214206
Limitations
215207
-----------
216208

217-
* If codejail or AppArmor is not configured properly, codejail will default to
209+
* If codejail or AppArmor is not configured properly, codejail may default to
218210
running code insecurely (no sandboxing). It is not secure by default.
219211
Projects integrating codejail should consider including a runtime test suite
220212
that checks for proper confinement at startup before untrusted inputs are

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)