From f0061817eaa62b1e3cbbbf66d328f3500be4553a Mon Sep 17 00:00:00 2001 From: Sebastion Date: Mon, 18 May 2026 17:53:39 +0100 Subject: [PATCH] fix: remove /debug/exec endpoint that allows authenticated RCE via exec() (#2178) The /api/v1/system/debug/exec endpoint passes user-supplied HTTP body directly to Python exec(), enabling arbitrary code execution for any authenticated user when debug_mode is enabled. This is a critical security risk (CWE-94): a single misconfiguration or compromised JWT grants full server-side code execution. Remove the endpoint entirely. The /debug/plugin/action endpoint (which does not use exec()) is left intact as it serves a different, scoped purpose. Co-authored-by: Junyan Chin --- .../pkg/api/http/controller/groups/system.py | 11 ---- tests/test_cwe94_debug_exec.py | 66 +++++++++++++++++++ 2 files changed, 66 insertions(+), 11 deletions(-) create mode 100644 tests/test_cwe94_debug_exec.py diff --git a/src/langbot/pkg/api/http/controller/groups/system.py b/src/langbot/pkg/api/http/controller/groups/system.py index 0cc5c990..e6c10fc0 100644 --- a/src/langbot/pkg/api/http/controller/groups/system.py +++ b/src/langbot/pkg/api/http/controller/groups/system.py @@ -140,17 +140,6 @@ class SystemRouterGroup(group.RouterGroup): async def _() -> str: return self.success(data=await self.ap.maintenance_service.get_storage_analysis()) - @self.route('/debug/exec', methods=['POST'], auth_type=group.AuthType.USER_TOKEN) - async def _() -> str: - if not constants.debug_mode: - return self.http_status(403, 403, 'Forbidden') - - py_code = await quart.request.data - - ap = self.ap - - return self.success(data=exec(py_code, {'ap': ap})) - @self.route( '/debug/plugin/action', methods=['POST'], diff --git a/tests/test_cwe94_debug_exec.py b/tests/test_cwe94_debug_exec.py new file mode 100644 index 00000000..48e08d1a --- /dev/null +++ b/tests/test_cwe94_debug_exec.py @@ -0,0 +1,66 @@ +""" +PoC test for CWE-94: Authenticated RCE via exec() on user-supplied Python code. + +The /api/v1/system/debug/exec endpoint passes raw HTTP body to exec(), +allowing arbitrary code execution when debug_mode is True. + +This test verifies that: +1. The exec() endpoint is removed from the codebase entirely. +2. No route matches /api/v1/system/debug/exec. +""" + +import ast +import pathlib + +# Resolve project root (one level up from tests/) +_PROJECT_ROOT = pathlib.Path(__file__).resolve().parent.parent + +VULN_FILE = ( + _PROJECT_ROOT + / "src" + / "langbot" + / "pkg" + / "api" + / "http" + / "controller" + / "groups" + / "system.py" +) + + +def test_no_exec_call_in_system_controller(): + """Verify there is no exec() call in system.py that takes user input.""" + with open(VULN_FILE, "r") as f: + source = f.read() + + tree = ast.parse(source) + + exec_calls = [] + for node in ast.walk(tree): + if isinstance(node, ast.Call): + func = node.func + # Match bare exec() call + if isinstance(func, ast.Name) and func.id == "exec": + exec_calls.append(node.lineno) + + assert len(exec_calls) == 0, ( + f"Found exec() call(s) at line(s) {exec_calls} in system.py. " + "User-supplied code must never be passed to exec()." + ) + + +def test_no_debug_exec_route(): + """Verify the /debug/exec route is not registered.""" + with open(VULN_FILE, "r") as f: + source = f.read() + + assert "debug/exec" not in source, ( + "The /debug/exec route still exists in system.py. " + "This endpoint allows arbitrary code execution and must be removed." + ) + + +if __name__ == "__main__": + test_no_exec_call_in_system_controller() + test_no_debug_exec_route() + print("All tests passed!")