From 292ba596d218d3e0d5718e6f2b9d2f3bce286c45 Mon Sep 17 00:00:00 2001 From: moose-lab Date: Thu, 2 Apr 2026 16:09:14 +0800 Subject: [PATCH] fix(sandbox): exclude URL paths from absolute path validation (#1385) (#1419) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(sandbox): URL路径被误判为不安全绝对路径 (#1385) 在本地沙箱模式下,bash工具对命令做绝对路径安全校验时,会把curl命令中的 HTTPS URL(如 https://example.com/api/v1/check)误识别为本地绝对路径并拦截。 根因:_ABSOLUTE_PATH_PATTERN 正则的负向后行断言 (? * fix(sandbox): refine absolute path regex to preserve file:// defense-in-depth Change lookbehind from (? --------- Signed-off-by: moose-lab Co-authored-by: moose-lab Co-authored-by: Claude Opus 4.6 (1M context) Co-authored-by: Willem Jiang --- .../harness/deerflow/sandbox/tools.py | 8 ++- backend/tests/test_sandbox_tools_security.py | 50 +++++++++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/backend/packages/harness/deerflow/sandbox/tools.py b/backend/packages/harness/deerflow/sandbox/tools.py index f7c07932..40d176a2 100644 --- a/backend/packages/harness/deerflow/sandbox/tools.py +++ b/backend/packages/harness/deerflow/sandbox/tools.py @@ -18,7 +18,8 @@ from deerflow.sandbox.sandbox import Sandbox from deerflow.sandbox.sandbox_provider import get_sandbox_provider from deerflow.sandbox.security import LOCAL_HOST_BASH_DISABLED_MESSAGE, is_host_bash_allowed -_ABSOLUTE_PATH_PATTERN = re.compile(r"(?()]+)") +_ABSOLUTE_PATH_PATTERN = re.compile(r"(?()]+)") +_FILE_URL_PATTERN = re.compile(r"\bfile://\S+", re.IGNORECASE) _LOCAL_BASH_SYSTEM_PATH_PREFIXES = ( "/bin/", "/usr/bin/", @@ -516,6 +517,11 @@ def validate_local_bash_command_paths(command: str, thread_data: ThreadDataState if thread_data is None: raise SandboxRuntimeError("Thread data not available for local sandbox") + # Block file:// URLs which bypass the absolute-path regex but allow local file exfiltration + file_url_match = _FILE_URL_PATTERN.search(command) + if file_url_match: + raise PermissionError(f"Unsafe file:// URL in command: {file_url_match.group()}. Use paths under {VIRTUAL_PATH_PREFIX}") + unsafe_paths: list[str] = [] allowed_paths = _get_mcp_allowed_paths() diff --git a/backend/tests/test_sandbox_tools_security.py b/backend/tests/test_sandbox_tools_security.py index 982b5d38..02d1b27c 100644 --- a/backend/tests/test_sandbox_tools_security.py +++ b/backend/tests/test_sandbox_tools_security.py @@ -324,6 +324,56 @@ def test_validate_local_bash_command_paths_allows_skills_path() -> None: ) +def test_validate_local_bash_command_paths_allows_urls() -> None: + """URLs in bash commands should not be mistaken for absolute paths (issue #1385).""" + # HTTPS URLs + validate_local_bash_command_paths( + "curl -X POST https://example.com/api/v1/risk/check", + _THREAD_DATA, + ) + # HTTP URLs + validate_local_bash_command_paths( + "curl http://localhost:8080/health", + _THREAD_DATA, + ) + # URLs with query strings + validate_local_bash_command_paths( + "curl https://api.example.com/v2/search?q=test", + _THREAD_DATA, + ) + # FTP URLs + validate_local_bash_command_paths( + "curl ftp://ftp.example.com/pub/file.tar.gz", + _THREAD_DATA, + ) + # URL mixed with valid virtual path + validate_local_bash_command_paths( + "curl https://example.com/data -o /mnt/user-data/workspace/data.json", + _THREAD_DATA, + ) + + +def test_validate_local_bash_command_paths_blocks_file_urls() -> None: + """file:// URLs should be treated as unsafe and blocked.""" + with pytest.raises(PermissionError): + validate_local_bash_command_paths("curl file:///etc/passwd", _THREAD_DATA) + + +def test_validate_local_bash_command_paths_blocks_file_urls_case_insensitive() -> None: + """file:// URL detection should be case-insensitive.""" + with pytest.raises(PermissionError): + validate_local_bash_command_paths("curl FILE:///etc/shadow", _THREAD_DATA) + + +def test_validate_local_bash_command_paths_blocks_file_urls_mixed_with_valid() -> None: + """file:// URLs should be blocked even when mixed with valid paths.""" + with pytest.raises(PermissionError): + validate_local_bash_command_paths( + "curl file:///etc/passwd -o /mnt/user-data/workspace/out.txt", + _THREAD_DATA, + ) + + def test_validate_local_bash_command_paths_still_blocks_other_paths() -> None: """Paths outside virtual and system prefixes must still be blocked.""" with patch("deerflow.sandbox.tools._get_skills_container_path", return_value="/mnt/skills"):