mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-02 03:55:55 +00:00
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 <rockchinq@gmail.com>
This commit is contained in:
@@ -140,17 +140,6 @@ class SystemRouterGroup(group.RouterGroup):
|
|||||||
async def _() -> str:
|
async def _() -> str:
|
||||||
return self.success(data=await self.ap.maintenance_service.get_storage_analysis())
|
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(
|
@self.route(
|
||||||
'/debug/plugin/action',
|
'/debug/plugin/action',
|
||||||
methods=['POST'],
|
methods=['POST'],
|
||||||
|
|||||||
66
tests/test_cwe94_debug_exec.py
Normal file
66
tests/test_cwe94_debug_exec.py
Normal file
@@ -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!")
|
||||||
Reference in New Issue
Block a user