From ce400f993c5003a6de13cdf857ba917f7f953b73 Mon Sep 17 00:00:00 2001 From: jasonwitty Date: Wed, 5 Aug 2026 23:05:24 -0700 Subject: [PATCH] [fix] plugin: ignore credentials in a user configured LLM server URL httpx derives an "Authorization: Basic" header from the userinfo of a URL, so a user could make SearXNG send a header of their choosing to a host of their choosing (e.g. to probe an internal service behind basic auth). A user preference carrying credentials is now ignored and the administrator default is used instead; credentials in the configured base_url are untouched. Co-Authored-By: Claude Opus 5 --- searx/plugins/ai_summary.py | 10 +++++++++- tests/unit/test_plugin_ai_summary.py | 11 +++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/searx/plugins/ai_summary.py b/searx/plugins/ai_summary.py index 7c2adb0f1..4ae36013d 100644 --- a/searx/plugins/ai_summary.py +++ b/searx/plugins/ai_summary.py @@ -152,7 +152,15 @@ def _server_api_key(cfg: SettingsAISummary, server: str) -> str: def _user_server(request: "SXNG_Request", cfg: SettingsAISummary) -> str: """The LLM server URL for this request: the user's ``ai_summary_server`` preference, or the administrator's default.""" - return str(request.preferences.get_value("ai_summary_server") or "").strip() or cfg.base_url + server = str(request.preferences.get_value("ai_summary_server") or "").strip() + # Credentials are stripped from a user's server URL: httpx turns them into + # an Authorization header, and a user should not be able to make SearXNG + # send a header of their choosing to a host of their choosing. An + # administrator can still use credentials in the configured base_url (e.g. + # an LLM server behind basic auth). + if server and "@" in urlparse(server).netloc: + server = "" + return server or cfg.base_url class SXNGPlugin(Plugin): diff --git a/tests/unit/test_plugin_ai_summary.py b/tests/unit/test_plugin_ai_summary.py index 1aadb75a4..01040ae08 100644 --- a/tests/unit/test_plugin_ai_summary.py +++ b/tests/unit/test_plugin_ai_summary.py @@ -349,6 +349,17 @@ class PluginAISummary(SearxTestCase): self.assertEqual(res.status_code, 200) self.assertEqual([("http://untrusted.example.org:11434", "")], calls) + def test_endpoint_ignores_credentials_in_user_server(self): + # httpx would turn the userinfo into an Authorization header; the + # preference is ignored and the admin's server is used instead + self.setattr4test(searx.get_setting("ai_summary"), "api_key", "sk-secret") + calls = self.mock_upstream_recording(sse_stream_mock([])) + self.client.set_cookie("ai_summary_server", "http://user:pass@untrusted.example.org:11434") + + res = self.client.post("/ai_summary", json={"messages": [{"role": "user", "content": "hi"}]}) + self.assertEqual(res.status_code, 200) + self.assertEqual([(BASE_URL, "sk-secret")], calls) + def test_endpoint_upstream_error(self): self.mock_upstream(sse_stream_mock([], status_code=500))