From 1a3df17dce6c22b4ba30f84b1094d7f8ae7fb0e0 Mon Sep 17 00:00:00 2001 From: anshul23102 Date: Tue, 26 May 2026 18:57:01 +0530 Subject: [PATCH 1/2] fix(executor): clamp user-supplied timeout to settings.sandbox_timeout _resolve_execution_timeout() accepted any positive integer from task inputs as the subprocess timeout, allowing a caller to override the operator-configured sandbox_timeout cap (default 600s) with an arbitrarily large value. A single request with max_scan_time set to INT_MAX could pin a worker for years and exhaust the concurrent task pool indefinitely. Change: apply min(timeout, settings.sandbox_timeout) so the resolved value never exceeds the operator cap. Callers may still request a shorter timeout than the default; they cannot request a longer one. Fixes #311 --- backend/secuscan/executor.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/backend/secuscan/executor.py b/backend/secuscan/executor.py index 01a68020..6afd1d19 100644 --- a/backend/secuscan/executor.py +++ b/backend/secuscan/executor.py @@ -558,7 +558,12 @@ async def read_stream(): return f"Execution error: {str(e)}", -1 def _resolve_execution_timeout(self, inputs: Dict[str, Any]) -> int: - """Resolve per-task process timeout from plugin inputs.""" + """Resolve per-task process timeout from plugin inputs. + + The caller may request a shorter timeout than the operator cap, but + never a longer one. ``settings.sandbox_timeout`` is the hard ceiling + and is always enforced regardless of what the client supplies. + """ for key in ("max_scan_time", "timeout"): raw_value = inputs.get(key) try: @@ -566,7 +571,7 @@ def _resolve_execution_timeout(self, inputs: Dict[str, Any]) -> int: except (TypeError, ValueError): continue if timeout > 0: - return timeout + return min(timeout, settings.sandbox_timeout) return settings.sandbox_timeout def _classify_command_result(self, plugin, output: str, exit_code: int) -> tuple[str, Optional[str]]: From d70f27372095f23c7bc4a04d0498b6af22485d2c Mon Sep 17 00:00:00 2001 From: Utkarsh Singh <183999732+utksh1@users.noreply.github.com> Date: Tue, 2 Jun 2026 01:04:54 +0530 Subject: [PATCH 2/2] test(executor): cover timeout clamp behavior --- testing/backend/unit/test_executor.py | 32 +++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/testing/backend/unit/test_executor.py b/testing/backend/unit/test_executor.py index f5a45c0f..f9c887ae 100644 --- a/testing/backend/unit/test_executor.py +++ b/testing/backend/unit/test_executor.py @@ -197,6 +197,38 @@ def test_classify_command_result_fails_on_undefined_flag_even_with_zero_exit(set assert error is not None +def test_resolve_execution_timeout_clamps_requested_timeout(monkeypatch): + monkeypatch.setattr(settings, "sandbox_timeout", 600) + + executor = TaskExecutor() + + assert executor._resolve_execution_timeout({"timeout": 9999}) == 600 + + +def test_resolve_execution_timeout_allows_shorter_requested_timeout(monkeypatch): + monkeypatch.setattr(settings, "sandbox_timeout", 600) + + executor = TaskExecutor() + + assert executor._resolve_execution_timeout({"timeout": 120}) == 120 + + +def test_resolve_execution_timeout_ignores_invalid_values(monkeypatch): + monkeypatch.setattr(settings, "sandbox_timeout", 600) + + executor = TaskExecutor() + + assert executor._resolve_execution_timeout({"timeout": "invalid"}) == 600 + + +def test_resolve_execution_timeout_prefers_max_scan_time(monkeypatch): + monkeypatch.setattr(settings, "sandbox_timeout", 600) + + executor = TaskExecutor() + + assert executor._resolve_execution_timeout({"max_scan_time": 90, "timeout": 120}) == 90 + + @pytest.mark.asyncio async def test_execute_task_sets_cancelled_status_in_db(setup_test_environment): """