Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ Changelog
but in reStructuredText instead of Markdown (for ease of incorporation into
Sphinx documentation and the PyPI description).

2025-06-16
**********
Changed
=======
* Upgrade to codejail 4.0.0, which no longer defaults to running in unsafe mode. This should make it a little harder for codejail-service to be misconfigured by accident.

2025-05-20
**********
Added
Expand Down
6 changes: 6 additions & 0 deletions codejail_service/apps/api/v0/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from os import path
from unittest.mock import call, patch

import codejail.safe_exec
import ddt
from django.test import TestCase, override_settings
from rest_framework.test import APIClient
Expand All @@ -31,10 +32,15 @@ def setUp(self):
#
# This approach means we can't parallelize the tests, but it's concise.
startup_check.STARTUP_SAFETY_CHECK_OK = True
# Tell codejail to just run any code in-process rather than trying to
# sandbox it (which it can't, in a generic developer environment).
codejail.safe_exec.ALWAYS_BE_UNSAFE = True
self.standard_params = {'code': 'retval = 3 + 4', 'globals_dict': {}}

def tearDown(self):
super().tearDown()
startup_check.STARTUP_SAFETY_CHECK_OK = None
codejail.safe_exec.ALWAYS_BE_UNSAFE = False

def test_missing_payload(self):
"""Handle missing payload param gracefully."""
Expand Down
13 changes: 2 additions & 11 deletions codejail_service/codejail.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
from copy import deepcopy
from json.decoder import JSONDecodeError

from codejail.safe_exec import SafeExecException
from codejail.safe_exec import safe_exec as real_safe_exec
from edx_django_utils.monitoring import record_exception

log = logging.getLogger(__name__)
Expand All @@ -27,17 +29,6 @@ def safe_exec(code, input_globals, **kwargs):
an error is raised, without requiring us to mutate the input. (And this
approach is much better for unit testing than the mutation option is.)
"""
# This needs to be a lazy import because as soon as codejail's
# safe_exec module loads, it immediately makes a decision about
# whether to run in always-unsafe mode.
#
# See https://github.com/openedx/codejail/issues/16 for maybe
# fixing this.

# pylint: disable=import-outside-toplevel
from codejail.safe_exec import SafeExecException
from codejail.safe_exec import safe_exec as real_safe_exec

# Prevent mutation of input
output_globals = deepcopy(input_globals)
try:
Expand Down
34 changes: 33 additions & 1 deletion codejail_service/tests/test_startup_check.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,33 @@
from unittest.mock import Mock, call, patch
from urllib.error import URLError

import codejail.safe_exec
import ddt
import pytest
from django.test import TestCase

from codejail_service import startup_check
from codejail_service.startup_check import is_exec_safe, run_startup_safety_check
from codejail_service.startup_check import _check_basic_function, is_exec_safe, run_startup_safety_check


class TestUnconfiguredCodejail(TestCase):

@patch('codejail_service.codejail.log.error')
def test_unconfigured(self, mock_log_error):
"""
Check that an unconfigured codejail refuses to run code.

In other tests we set `ALWAYS_BE_UNSAFE = True` so that we can test the
integration with the codejail library, but here we leave that off to ensure
that we're using a version of codejail that behaves (more) safely by
default.
"""
assert _check_basic_function() == "Unexpected error: Couldn't execute sandboxed code: See logs."
mock_log_error.assert_called_once_with(
"Unexpected error type from safe_exec: "
"RuntimeError('safe_exec has not been configured for Python')",
exc_info=True
)


class TestStateCheck(TestCase):
Expand Down Expand Up @@ -61,6 +83,16 @@ def responses(math=DEFAULT, disk=DEFAULT, child=DEFAULT, network=DEFAULT):
@ddt.ddt
class TestInit(TestCase):

def setUp(self):
super().setUp()
# Tell codejail to just run any code in-process rather than trying to
# sandbox it (which it can't, in a generic developer environment).
codejail.safe_exec.ALWAYS_BE_UNSAFE = True

def tearDown(self):
super().tearDown()
codejail.safe_exec.ALWAYS_BE_UNSAFE = False

@patch('codejail_service.startup_check.STARTUP_SAFETY_CHECK_OK', None)
def test_unsafe_tests_default(self):
"""
Expand Down
5 changes: 5 additions & 0 deletions docs/decisions/0001-purpose-of-this-repo.rst
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ In 2021 eduNEXT had previously implemented a Flask-based remote codejail service

In 2025, 2U made a push to move its own deployment of edx-platform from the legacy Ansible and EC2 based build system to a Docker and Kubernetes system. In the process, 2U wanted to move to a remote codejail for both security and ease of deployment reasons.

Updates
=======

- 2025-06-16: As of codejail 4.0.0, it is no longer true that the library defaults to running without any attempt at confinement. (However, it still has no way of protecting against missing or misconfigured AppArmor.)

Decision
********

Expand Down
3 changes: 2 additions & 1 deletion requirements/base.in
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ Django # Web application framework
gunicorn # HTTP server framework
edx-django-utils # Monitoring utilities, among other things
edx-toggles # Feature switch tools
edx-codejail # Actual codejail library
# Codejail 3 and below will automatically run in unsafe mode if unconfigured.
edx-codejail>=4.0.0 # Actual codejail library
djangorestframework # Request parsing and response formatting
jsonschema # Parse and validate JSON
2 changes: 1 addition & 1 deletion requirements/base.txt
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ django-waffle==4.2.0
# edx-toggles
djangorestframework==3.16.0
# via -r requirements/base.in
edx-codejail==3.5.2
edx-codejail==4.0.0
# via -r requirements/base.in
edx-django-utils==8.0.0
# via
Expand Down
2 changes: 1 addition & 1 deletion requirements/dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ docutils==0.21.2
# via
# -r requirements/validation.txt
# readme-renderer
edx-codejail==3.5.2
edx-codejail==4.0.0
# via -r requirements/validation.txt
edx-django-utils==8.0.0
# via
Expand Down
2 changes: 1 addition & 1 deletion requirements/doc.txt
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ docutils==0.21.2
# pydata-sphinx-theme
# restructuredtext-lint
# sphinx
edx-codejail==3.5.2
edx-codejail==4.0.0
# via -r requirements/test.txt
edx-django-utils==8.0.0
# via
Expand Down
2 changes: 1 addition & 1 deletion requirements/quality.txt
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ djangorestframework==3.16.0
# via -r requirements/test.txt
docutils==0.21.2
# via readme-renderer
edx-codejail==3.5.2
edx-codejail==4.0.0
# via -r requirements/test.txt
edx-django-utils==8.0.0
# via
Expand Down
2 changes: 1 addition & 1 deletion requirements/test.txt
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ django-waffle==4.2.0
# edx-toggles
djangorestframework==3.16.0
# via -r requirements/base.txt
edx-codejail==3.5.2
edx-codejail==4.0.0
# via -r requirements/base.txt
edx-django-utils==8.0.0
# via
Expand Down
2 changes: 1 addition & 1 deletion requirements/validation.txt
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ docutils==0.21.2
# via
# -r requirements/quality.txt
# readme-renderer
edx-codejail==3.5.2
edx-codejail==4.0.0
# via
# -r requirements/quality.txt
# -r requirements/test.txt
Expand Down