Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .secrets.baseline
Original file line number Diff line number Diff line change
Expand Up @@ -3565,7 +3565,7 @@
"filename": "src/backend/tests/unit/components/languagemodels/test_xai.py",
"hashed_secret": "3bee216ebc256d692260fc3adc765050508fef5e",
"is_verified": false,
"line_number": 150,
"line_number": 159,
"is_secret": false
}
],
Expand Down Expand Up @@ -9287,5 +9287,5 @@
}
]
},
"generated_at": "2026-06-10T08:36:23Z"
"generated_at": "2026-06-12T22:46:30Z"
}

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@
from lfx.schema.message import Message


@pytest.fixture(autouse=True)
def allow_local_ollama(monkeypatch):
monkeypatch.setenv("LANGFLOW_SSRF_ALLOWED_HOSTS", "localhost")


@pytest.mark.integration
class TestChatOllamaIntegration:
"""Integration tests for ChatOllama structured output flow."""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -919,3 +919,7 @@ def test_component_outputs(self, component_class):
component = component_class()
output_names = [out.name for out in component.outputs]
assert "embeddings" in output_names

@pytest.fixture(autouse=True)
def disable_ssrf_protection(self, monkeypatch):
monkeypatch.setenv("LANGFLOW_SSRF_PROTECTION_ENABLED", "false")
Original file line number Diff line number Diff line change
Expand Up @@ -1260,3 +1260,7 @@ def test_build_model_structured_output_toggle_behavior(self, mock_chat_ollama, c
assert "format" in call_args, "format should be in call when enabled"
assert call_args["format"] == "json"
assert model == mock_instance

@pytest.fixture(autouse=True)
def disable_ssrf_protection(self, monkeypatch):
monkeypatch.setenv("LANGFLOW_SSRF_PROTECTION_ENABLED", "false")
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,8 @@ def test_deepseek_build_model(mock_chat_openai, temperature, max_tokens):
def test_deepseek_get_models(mocker):
component = DeepSeekModelComponent()

# Mock requests.get
mock_get = mocker.patch("requests.get")
# Mock SSRF-safe httpx helper
mock_get = mocker.patch("lfx.components.deepseek.deepseek.ssrf_safe_httpx_get")
mock_response = MagicMock()
mock_response.json.return_value = {"data": [{"id": "deepseek-chat"}, {"id": "deepseek-coder"}]}
mock_get.return_value = mock_response
Expand All @@ -110,3 +110,8 @@ def test_deepseek_error_handling(mock_chat_openai):

with pytest.raises(Exception, match="Invalid API key"):
component.build_model()


@pytest.fixture(autouse=True)
def disable_ssrf_protection(monkeypatch):
monkeypatch.setenv("LANGFLOW_SSRF_PROTECTION_ENABLED", "false")
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ def test_build_model(self, component_class, default_kwargs, mocker):
component = component_class(**default_kwargs)

mocker.patch(
"lfx.components.litellm.litellm_proxy.httpx.get",
"lfx.components.litellm.litellm_proxy.ssrf_safe_httpx_get",
return_value=_mock_models_response(),
)
mock_chat_openai = mocker.patch(
Expand All @@ -98,7 +98,7 @@ def test_build_model_secret_str_api_key(self, component_class, default_kwargs, m
component = component_class(**default_kwargs)

mocker.patch(
"lfx.components.litellm.litellm_proxy.httpx.get",
"lfx.components.litellm.litellm_proxy.ssrf_safe_httpx_get",
return_value=_mock_models_response(),
)
mock_chat_openai = mocker.patch(
Expand All @@ -116,7 +116,7 @@ def test_build_model_max_tokens_zero(self, component_class, default_kwargs, mock
component = component_class(**default_kwargs)

mocker.patch(
"lfx.components.litellm.litellm_proxy.httpx.get",
"lfx.components.litellm.litellm_proxy.ssrf_safe_httpx_get",
return_value=_mock_models_response(),
)
mock_chat_openai = mocker.patch(
Expand All @@ -133,7 +133,7 @@ def test_build_model_max_tokens_zero(self, component_class, default_kwargs, mock
def test_validate_proxy_connection_success(self, component_class, default_kwargs, mocker):
component = component_class(**default_kwargs)
mocker.patch(
"lfx.components.litellm.litellm_proxy.httpx.get",
"lfx.components.litellm.litellm_proxy.ssrf_safe_httpx_get",
return_value=_mock_models_response(),
)
# Should not raise
Expand All @@ -142,7 +142,7 @@ def test_validate_proxy_connection_success(self, component_class, default_kwargs
def test_validate_proxy_connection_auth_failure(self, component_class, default_kwargs, mocker):
component = component_class(**default_kwargs)
mocker.patch(
"lfx.components.litellm.litellm_proxy.httpx.get",
"lfx.components.litellm.litellm_proxy.ssrf_safe_httpx_get",
return_value=_mock_models_response(status_code=401),
)
with pytest.raises(ValueError, match="Authentication failed"):
Expand All @@ -152,7 +152,7 @@ def test_validate_proxy_connection_model_not_found(self, component_class, defaul
default_kwargs["model_name"] = "invalid-model-name"
component = component_class(**default_kwargs)
mocker.patch(
"lfx.components.litellm.litellm_proxy.httpx.get",
"lfx.components.litellm.litellm_proxy.ssrf_safe_httpx_get",
return_value=_mock_models_response(models=[{"id": "gpt-4o"}]),
)
with pytest.raises(ValueError, match=r"invalid-model-name.*not found"):
Expand All @@ -161,7 +161,7 @@ def test_validate_proxy_connection_model_not_found(self, component_class, defaul
def test_validate_proxy_connection_connect_error(self, component_class, default_kwargs, mocker):
component = component_class(**default_kwargs)
mocker.patch(
"lfx.components.litellm.litellm_proxy.httpx.get",
"lfx.components.litellm.litellm_proxy.ssrf_safe_httpx_get",
side_effect=httpx.ConnectError("Connection refused"),
)
with pytest.raises(ValueError, match="Could not connect"):
Expand All @@ -170,7 +170,7 @@ def test_validate_proxy_connection_connect_error(self, component_class, default_
def test_validate_proxy_connection_timeout(self, component_class, default_kwargs, mocker):
component = component_class(**default_kwargs)
mocker.patch(
"lfx.components.litellm.litellm_proxy.httpx.get",
"lfx.components.litellm.litellm_proxy.ssrf_safe_httpx_get",
side_effect=httpx.TimeoutException("Timed out"),
)
with pytest.raises(ValueError, match="timed out"):
Expand All @@ -179,7 +179,7 @@ def test_validate_proxy_connection_timeout(self, component_class, default_kwargs
def test_validate_proxy_connection_empty_models_list(self, component_class, default_kwargs, mocker):
component = component_class(**default_kwargs)
mocker.patch(
"lfx.components.litellm.litellm_proxy.httpx.get",
"lfx.components.litellm.litellm_proxy.ssrf_safe_httpx_get",
return_value=_mock_models_response(models=[]),
)
# Empty models list should not raise (proxy may not report models)
Expand Down Expand Up @@ -238,3 +238,7 @@ def test_get_exception_message_no_openai_import(self, component_class, default_k
with patch.dict("sys.modules", {"openai": None}):
message = component._get_exception_message(Exception("test"))
assert message is None

@pytest.fixture(autouse=True)
def disable_ssrf_protection(self, monkeypatch):
monkeypatch.setenv("LANGFLOW_SSRF_PROTECTION_ENABLED", "false")
22 changes: 17 additions & 5 deletions src/backend/tests/unit/components/languagemodels/test_xai.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,15 @@ def test_build_model(self, component_class, default_kwargs, mocker):
component.base_url = "https://api.x.ai/v1"
component.seed = 1

http_client = MagicMock()
http_async_client = MagicMock()
mock_client_kwargs = mocker.patch(
"lfx.components.xai.xai.ssrf_protected_openai_clients_for_url",
return_value={"http_client": http_client, "http_async_client": http_async_client},
)
mock_chat_openai = mocker.patch("lfx.components.xai.xai.ChatOpenAI", return_value=MagicMock())
model = component.build_model()
mock_client_kwargs.assert_called_once_with("https://api.x.ai/v1")
mock_chat_openai.assert_called_once_with(
max_tokens=100,
model_kwargs={},
Expand All @@ -111,12 +118,14 @@ def test_build_model(self, component_class, default_kwargs, mocker):
api_key="test-key",
temperature=0.7,
seed=1,
http_client=http_client,
http_async_client=http_async_client,
)
assert model == mock_chat_openai.return_value

def test_get_models(self):
component = XAIModelComponent()
with patch("requests.get") as mock_get:
with patch("lfx.components.xai.xai.ssrf_safe_httpx_get") as mock_get:
mock_response = MagicMock()
mock_response.json.return_value = {
"models": [
Expand Down Expand Up @@ -191,8 +200,11 @@ def test_update_build_config(self):
component = XAIModelComponent()
build_config = {"model_name": {"options": []}}

updated_config = component.update_build_config(build_config, "test-key", "api_key")
assert "model_name" in updated_config
with patch.object(component, "get_models", return_value=["grok-2-latest"]) as mock_get_models:
updated_config = component.update_build_config(build_config, "test-key", "api_key")
assert "model_name" in updated_config

updated_config = component.update_build_config(build_config, "grok-2-latest", "model_name")
assert "model_name" in updated_config

updated_config = component.update_build_config(build_config, "grok-2-latest", "model_name")
assert "model_name" in updated_config
assert mock_get_models.call_count == 2
183 changes: 183 additions & 0 deletions src/backend/tests/unit/components/test_ssrf_guarded_url_components.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
from unittest.mock import patch

import pytest
from lfx.components.deepseek.deepseek import DEEPSEEK_MODELS, DeepSeekModelComponent
from lfx.components.glean.glean_search_api import GleanAPIWrapper
from lfx.components.homeassistant.home_assistant_control import HomeAssistantControl
from lfx.components.homeassistant.list_home_assistant_states import ListHomeAssistantStates
from lfx.components.huggingface.huggingface_inference_api import HuggingFaceInferenceAPIEmbeddingsComponent
from lfx.components.litellm.litellm_proxy import LiteLLMProxyComponent
from lfx.components.lmstudio.lmstudioembeddings import LMStudioEmbeddingsComponent
from lfx.components.lmstudio.lmstudiomodel import LMStudioModelComponent
from lfx.components.ollama.ollama import ChatOllamaComponent
from lfx.components.ollama.ollama_embeddings import OllamaEmbeddingsComponent
from lfx.components.xai.xai import XAI_DEFAULT_MODELS, XAIModelComponent
from lfx.utils.ssrf_protection import SSRFProtectionError

BLOCKED_URL = "http://169.254.169.254/latest/meta-data"


@pytest.fixture(autouse=True)
def enable_ssrf_protection(monkeypatch):
monkeypatch.setenv("LANGFLOW_SSRF_PROTECTION_ENABLED", "true")
monkeypatch.delenv("LANGFLOW_SSRF_ALLOWED_HOSTS", raising=False)


@pytest.mark.asyncio
async def test_lmstudio_model_update_blocks_metadata_url_before_httpx():
component = LMStudioModelComponent()
build_config = {"base_url": {"load_from_db": False, "value": BLOCKED_URL}, "model_name": {"options": []}}

with patch("httpx.AsyncClient.get") as mock_get, pytest.raises(ValueError, match="SSRF Protection"):
await component.update_build_config(build_config, None, "model_name")

mock_get.assert_not_called()


def test_lmstudio_model_build_blocks_metadata_url_before_openai_client():
component = LMStudioModelComponent(base_url=BLOCKED_URL, model_name="model", api_key="test")

with (
patch("lfx.components.lmstudio.lmstudiomodel.ChatOpenAI") as mock_chat_openai,
pytest.raises(ValueError, match="SSRF Protection"),
):
component.build_model()

mock_chat_openai.assert_not_called()


def test_lmstudio_embeddings_build_blocks_metadata_url_before_sdk_client():
component = LMStudioEmbeddingsComponent(base_url=BLOCKED_URL, model="model", api_key="test")

with (
patch("lfx.components.lmstudio.lmstudioembeddings.NVIDIAEmbeddings", create=True) as mock_embeddings,
pytest.raises(ValueError, match="SSRF Protection"),
):
component.build_embeddings()

mock_embeddings.assert_not_called()


def test_home_assistant_list_states_blocks_metadata_url_before_httpx():
component = ListHomeAssistantStates()

with patch("httpx.Client.get") as mock_get:
result = component._list_states("token", BLOCKED_URL)

assert "SSRF Protection" in result
mock_get.assert_not_called()


def test_home_assistant_control_blocks_metadata_url_before_httpx():
component = HomeAssistantControl()

with patch("httpx.Client.post") as mock_post:
result = component._control_device("token", BLOCKED_URL, "turn_on", "switch.test")

assert "SSRF Protection" in result
mock_post.assert_not_called()


def test_deepseek_model_fetch_blocks_metadata_url_before_httpx():
component = DeepSeekModelComponent(api_base=BLOCKED_URL, api_key="test")

with patch("httpx.Client.get") as mock_get:
models = component.get_models()

assert models == DEEPSEEK_MODELS
assert "SSRF Protection" in component.status
mock_get.assert_not_called()


def test_deepseek_build_blocks_metadata_url_before_openai_client():
component = DeepSeekModelComponent(api_base=BLOCKED_URL, api_key="test")

with patch("langchain_openai.ChatOpenAI") as mock_chat_openai, pytest.raises(ValueError, match="SSRF Protection"):
component.build_model()

mock_chat_openai.assert_not_called()


def test_xai_model_fetch_blocks_metadata_url_before_httpx():
component = XAIModelComponent(base_url=BLOCKED_URL, api_key="test")

with patch("httpx.Client.get") as mock_get:
models = component.get_models()

assert models == XAI_DEFAULT_MODELS
assert "SSRF Protection" in component.status
mock_get.assert_not_called()


def test_xai_build_blocks_metadata_url_before_openai_client():
component = XAIModelComponent(base_url=BLOCKED_URL, api_key="test")

with (
patch("lfx.components.xai.xai.ChatOpenAI") as mock_chat_openai,
pytest.raises(ValueError, match="SSRF Protection"),
):
component.build_model()

mock_chat_openai.assert_not_called()


def test_glean_blocks_metadata_url_before_httpx_post():
wrapper = GleanAPIWrapper(glean_api_url=BLOCKED_URL, glean_access_token="test-access-token") # noqa: S106

with patch("httpx.Client.post") as mock_post, pytest.raises(SSRFProtectionError):
wrapper._search_api_results("query")

mock_post.assert_not_called()


def test_huggingface_build_blocks_metadata_url_before_sdk_client():
component = HuggingFaceInferenceAPIEmbeddingsComponent(
inference_endpoint=BLOCKED_URL,
model_name="model",
)

with (
patch.object(component, "create_huggingface_embeddings") as mock_create,
pytest.raises(ValueError, match="SSRF Protection"),
):
component.build_embeddings()

mock_create.assert_not_called()


def test_ollama_embeddings_build_blocks_metadata_url_before_sdk_client():
component = OllamaEmbeddingsComponent(base_url=BLOCKED_URL, model_name="model")

with (
patch("lfx.components.ollama.ollama_embeddings.OllamaEmbeddings") as mock_embeddings,
pytest.raises(ValueError, match="SSRF Protection"),
):
component.build_embeddings()

mock_embeddings.assert_not_called()


def test_ollama_build_blocks_metadata_url_before_sdk_client():
component = ChatOllamaComponent(base_url=BLOCKED_URL, model_name="model", mirostat="Disabled")

with (
patch("lfx.components.ollama.ollama.ChatOllama") as mock_chat_ollama,
pytest.raises(ValueError, match="SSRF Protection"),
):
component.build_model()

mock_chat_ollama.assert_not_called()


def test_litellm_build_blocks_metadata_url_before_httpx_and_openai_client():
component = LiteLLMProxyComponent(api_base=BLOCKED_URL, api_key="test", model_name="model")

with (
patch("httpx.Client.get") as mock_get,
patch("lfx.components.litellm.litellm_proxy.ChatOpenAI") as mock_chat_openai,
pytest.raises(ValueError, match="SSRF Protection"),
):
component.build_model()

mock_get.assert_not_called()
mock_chat_openai.assert_not_called()
Loading
Loading