mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-05-04 16:52:37 +00:00
CRITICAL SECURITY FIX ===================== Vulnerability: Sandbox escape in CodeInterpreterTool fallback leads to host RCE Impact: - Removed bypassable Python sandbox that could be escaped via object introspection - Attackers could previously execute arbitrary code on host when Docker unavailable Changes: - Removed SandboxPython class entirely (insecure by design) - Removed run_code_in_restricted_sandbox() fallback method - Implemented fail-safe behavior: raises RuntimeError when Docker unavailable - Fixed command injection in unsafe_mode library installation (os.system -> subprocess) - Enhanced security warnings and documentation Security Model: - Safe mode (default): Requires Docker, fails safely if unavailable - Unsafe mode: Explicit opt-in, clear warnings, no protections Breaking Change: - Code execution now requires Docker or explicit unsafe_mode=True - Previous silent fallback to vulnerable sandbox is removed Testing: - Updated all tests to reflect new fail-safe behavior - Added tests for Docker unavailable scenarios - Verified subprocess usage for library installation Refs: F-001, SECURITY_FIX_F001.md Docs: https://docs.crewai.com/en/tools/ai-ml/codeinterpretertool Co-authored-by: Rip&Tear <theCyberTech@users.noreply.github.com>
168 lines
5.4 KiB
Python
168 lines
5.4 KiB
Python
import subprocess
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
|
|
from crewai_tools.tools.code_interpreter_tool.code_interpreter_tool import (
|
|
CodeInterpreterTool,
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def printer_mock():
|
|
with patch("crewai_tools.printer.Printer.print") as mock:
|
|
yield mock
|
|
|
|
|
|
@pytest.fixture
|
|
def docker_unavailable_mock():
|
|
with patch(
|
|
"crewai_tools.tools.code_interpreter_tool.code_interpreter_tool.CodeInterpreterTool._check_docker_available",
|
|
return_value=False,
|
|
) as mock:
|
|
yield mock
|
|
|
|
|
|
@patch("crewai_tools.tools.code_interpreter_tool.code_interpreter_tool.docker_from_env")
|
|
def test_run_code_in_docker(docker_mock, printer_mock):
|
|
tool = CodeInterpreterTool()
|
|
code = "print('Hello, World!')"
|
|
libraries_used = ["numpy", "pandas"]
|
|
expected_output = "Hello, World!\n"
|
|
|
|
docker_mock().containers.run().exec_run().exit_code = 0
|
|
docker_mock().containers.run().exec_run().output = expected_output.encode()
|
|
|
|
result = tool.run_code_in_docker(code, libraries_used)
|
|
assert result == expected_output
|
|
printer_mock.assert_called_with(
|
|
"Running code in Docker environment", color="bold_blue"
|
|
)
|
|
|
|
|
|
@patch("crewai_tools.tools.code_interpreter_tool.code_interpreter_tool.docker_from_env")
|
|
def test_run_code_in_docker_with_error(docker_mock, printer_mock):
|
|
tool = CodeInterpreterTool()
|
|
code = "print(1/0)"
|
|
libraries_used = ["numpy", "pandas"]
|
|
expected_output = "Something went wrong while running the code: \nZeroDivisionError: division by zero\n"
|
|
|
|
docker_mock().containers.run().exec_run().exit_code = 1
|
|
docker_mock().containers.run().exec_run().output = (
|
|
b"ZeroDivisionError: division by zero\n"
|
|
)
|
|
|
|
result = tool.run_code_in_docker(code, libraries_used)
|
|
assert result == expected_output
|
|
printer_mock.assert_called_with(
|
|
"Running code in Docker environment", color="bold_blue"
|
|
)
|
|
|
|
|
|
@patch("crewai_tools.tools.code_interpreter_tool.code_interpreter_tool.docker_from_env")
|
|
def test_run_code_in_docker_with_script(docker_mock, printer_mock):
|
|
tool = CodeInterpreterTool()
|
|
code = """print("This is line 1")
|
|
print("This is line 2")"""
|
|
libraries_used = []
|
|
expected_output = "This is line 1\nThis is line 2\n"
|
|
|
|
docker_mock().containers.run().exec_run().exit_code = 0
|
|
docker_mock().containers.run().exec_run().output = expected_output.encode()
|
|
|
|
result = tool.run_code_in_docker(code, libraries_used)
|
|
assert result == expected_output
|
|
printer_mock.assert_called_with(
|
|
"Running code in Docker environment", color="bold_blue"
|
|
)
|
|
|
|
|
|
def test_docker_unavailable_fails_safely(printer_mock, docker_unavailable_mock):
|
|
"""Test that code execution fails when Docker is unavailable."""
|
|
tool = CodeInterpreterTool()
|
|
code = """
|
|
result = 2 + 2
|
|
print(result)
|
|
"""
|
|
with pytest.raises(RuntimeError) as exc_info:
|
|
tool.run(code=code, libraries_used=[])
|
|
|
|
assert "Docker is required for safe code execution" in str(exc_info.value)
|
|
assert printer_mock.called
|
|
call_args = printer_mock.call_args
|
|
assert "SECURITY ERROR" in call_args[0][0]
|
|
assert call_args[1]["color"] == "bold_red"
|
|
|
|
|
|
def test_docker_unavailable_suggests_unsafe_mode(printer_mock, docker_unavailable_mock):
|
|
"""Test that error message suggests unsafe_mode as alternative."""
|
|
tool = CodeInterpreterTool()
|
|
code = "result = 1 + 1"
|
|
|
|
with pytest.raises(RuntimeError) as exc_info:
|
|
tool.run(code=code, libraries_used=[])
|
|
|
|
error_output = printer_mock.call_args[0][0]
|
|
assert "unsafe_mode=True" in error_output
|
|
assert "NOT recommended" in error_output
|
|
assert "docs.crewai.com" in error_output
|
|
|
|
|
|
def test_unsafe_mode_running_with_no_result_variable(
|
|
printer_mock, docker_unavailable_mock
|
|
):
|
|
"""Test behavior when no result variable is set in unsafe mode."""
|
|
tool = CodeInterpreterTool(unsafe_mode=True)
|
|
code = """
|
|
x = 10
|
|
"""
|
|
result = tool.run(code=code, libraries_used=[])
|
|
printer_mock.assert_called_with(
|
|
"⚠️ WARNING: Running code in UNSAFE mode - no security controls active!",
|
|
color="bold_red",
|
|
)
|
|
assert result == "No result variable found."
|
|
|
|
|
|
def test_unsafe_mode_running_unsafe_code(printer_mock, docker_unavailable_mock):
|
|
"""Test that unsafe mode allows unrestricted code execution."""
|
|
tool = CodeInterpreterTool(unsafe_mode=True)
|
|
code = """
|
|
import os
|
|
result = eval("5/1")
|
|
"""
|
|
result = tool.run(code=code, libraries_used=[])
|
|
printer_mock.assert_called_with(
|
|
"⚠️ WARNING: Running code in UNSAFE mode - no security controls active!",
|
|
color="bold_red",
|
|
)
|
|
assert 5.0 == result
|
|
|
|
|
|
@patch("crewai_tools.tools.code_interpreter_tool.code_interpreter_tool.subprocess.run")
|
|
def test_unsafe_mode_library_installation(subprocess_mock, printer_mock, docker_unavailable_mock):
|
|
"""Test that unsafe mode properly installs libraries using subprocess."""
|
|
tool = CodeInterpreterTool(unsafe_mode=True)
|
|
code = "result = 42"
|
|
libraries = ["numpy", "pandas"]
|
|
|
|
subprocess_mock.return_value = None
|
|
|
|
tool.run(code=code, libraries_used=libraries)
|
|
|
|
assert subprocess_mock.call_count == 2
|
|
subprocess_mock.assert_any_call(
|
|
["pip", "install", "numpy"],
|
|
check=True,
|
|
stdout=subprocess.DEVNULL,
|
|
stderr=subprocess.DEVNULL,
|
|
timeout=30,
|
|
)
|
|
subprocess_mock.assert_any_call(
|
|
["pip", "install", "pandas"],
|
|
check=True,
|
|
stdout=subprocess.DEVNULL,
|
|
stderr=subprocess.DEVNULL,
|
|
timeout=30,
|
|
)
|