From 9c3803828b91e5285da037ae697d1d5e643bda2c Mon Sep 17 00:00:00 2001 From: xbsheng Date: Mon, 16 Mar 2026 10:41:02 +0800 Subject: [PATCH 1/2] fix: add SSRF check in WebSearchTool to prevent unsafe URL access --- src/openjarvis/tools/web_search.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/openjarvis/tools/web_search.py b/src/openjarvis/tools/web_search.py index 0b01f8cc..430e58b3 100644 --- a/src/openjarvis/tools/web_search.py +++ b/src/openjarvis/tools/web_search.py @@ -8,6 +8,7 @@ from typing import Any from openjarvis.core.registry import ToolRegistry from openjarvis.core.types import ToolResult from openjarvis.tools._stubs import BaseTool, ToolSpec +from openjarvis.security.ssrf import check_ssrf @ToolRegistry.register("web_search") @@ -76,6 +77,9 @@ class WebSearchTool(BaseTool): import httpx url = WebSearchTool._normalize_url(url) + ssrf_error = check_ssrf(url) + if ssrf_error: + raise ValueError(ssrf_error) resp = httpx.get( url.strip(), follow_redirects=True, From 2a13505a1c3391fa6fd95a74ac25f224a6714894 Mon Sep 17 00:00:00 2001 From: Jon Saad-Falcon <41205309+jonsaadfalcon@users.noreply.github.com> Date: Sun, 15 Mar 2026 21:18:46 -0700 Subject: [PATCH 2/2] fix: sort imports and mock SSRF check in web_search tests Sort the check_ssrf import alphabetically to fix ruff I001. Mock the SSRF check in URL-fetching tests since it requires the Rust backend which isn't available in CI. Add a dedicated test verifying SSRF rejection. --- src/openjarvis/tools/web_search.py | 2 +- tests/tools/test_web_search.py | 32 ++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/src/openjarvis/tools/web_search.py b/src/openjarvis/tools/web_search.py index 430e58b3..227d76aa 100644 --- a/src/openjarvis/tools/web_search.py +++ b/src/openjarvis/tools/web_search.py @@ -7,8 +7,8 @@ from typing import Any from openjarvis.core.registry import ToolRegistry from openjarvis.core.types import ToolResult -from openjarvis.tools._stubs import BaseTool, ToolSpec from openjarvis.security.ssrf import check_ssrf +from openjarvis.tools._stubs import BaseTool, ToolSpec @ToolRegistry.register("web_search") diff --git a/tests/tools/test_web_search.py b/tests/tools/test_web_search.py index 1fef8fb2..d5e6ce3e 100644 --- a/tests/tools/test_web_search.py +++ b/tests/tools/test_web_search.py @@ -205,10 +205,17 @@ class TestUrlNormalization: class TestUrlFetching: + def _mock_ssrf(self, monkeypatch): + """Stub out the SSRF check (requires Rust backend).""" + import openjarvis.tools.web_search as _ws + + monkeypatch.setattr(_ws, "check_ssrf", lambda url: None) + def test_fetch_url_success(self, monkeypatch): """Mocked HTTP GET returns HTML, stripped to text.""" import httpx + self._mock_ssrf(monkeypatch) mock_resp = MagicMock() mock_resp.text = "

Hello world

" mock_resp.headers = {"content-type": "text/html"} @@ -221,6 +228,7 @@ class TestUrlFetching: def test_fetch_url_strips_scripts(self, monkeypatch): import httpx + self._mock_ssrf(monkeypatch) mock_resp = MagicMock() mock_resp.text = ( "Content" @@ -236,6 +244,7 @@ class TestUrlFetching: def test_fetch_url_truncates_long_content(self, monkeypatch): import httpx + self._mock_ssrf(monkeypatch) mock_resp = MagicMock() mock_resp.text = "

" + "x" * 10000 + "

" mock_resp.headers = {"content-type": "text/html"} @@ -249,6 +258,7 @@ class TestUrlFetching: def test_fetch_url_pdf_content_type(self, monkeypatch): import httpx + self._mock_ssrf(monkeypatch) mock_resp = MagicMock() mock_resp.text = "%PDF-1.4 binary data" mock_resp.headers = {"content-type": "application/pdf"} @@ -261,10 +271,17 @@ class TestUrlFetching: class TestExecuteWithUrl: + def _mock_ssrf(self, monkeypatch): + """Stub out the SSRF check (requires Rust backend).""" + import openjarvis.tools.web_search as _ws + + monkeypatch.setattr(_ws, "check_ssrf", lambda url: None) + def test_execute_with_url_query(self, monkeypatch): """When query is a URL, fetch instead of search.""" import httpx + self._mock_ssrf(monkeypatch) mock_resp = MagicMock() mock_resp.text = "Page content here" mock_resp.headers = {"content-type": "text/html"} @@ -281,6 +298,7 @@ class TestExecuteWithUrl: """When query contains a URL within text, detect and fetch it.""" import httpx + self._mock_ssrf(monkeypatch) mock_resp = MagicMock() mock_resp.text = "Article text" mock_resp.headers = {"content-type": "text/html"} @@ -294,10 +312,24 @@ class TestExecuteWithUrl: assert result.success is True assert result.metadata.get("mode") == "fetch" + def test_execute_url_ssrf_blocked(self, monkeypatch): + """SSRF check rejects unsafe URLs before any HTTP request.""" + import openjarvis.tools.web_search as _ws + + monkeypatch.setattr( + _ws, "check_ssrf", lambda url: "private IP blocked", + ) + + tool = WebSearchTool(api_key="test-key") + result = tool.execute(query="http://169.254.169.254/metadata") + assert result.success is False + assert "private IP blocked" in result.content + def test_execute_url_fetch_failure(self, monkeypatch): """URL fetch failure returns error result.""" import httpx + self._mock_ssrf(monkeypatch) monkeypatch.setattr( httpx, "get", MagicMock(side_effect=httpx.HTTPError("Connection failed")),