From e85d08033cdb0575378ada54c07831e894f1c2e9 Mon Sep 17 00:00:00 2001 From: Michal Kulakowski Date: Fri, 16 Jan 2026 19:35:43 +0100 Subject: [PATCH 1/6] Support allowed domains parameter --- .bazelrc | 1 + docs/model_server_rest_api_chat.md | 1 + docs/parameters.md | 3 +- docs/security_considerations.md | 2 + src/capi_frontend/server_settings.hpp | 1 + src/cli_parser.cpp | 7 + src/llm/apis/openai_completions.cpp | 48 ++++- src/llm/apis/openai_completions.hpp | 6 +- .../visual_language_model/legacy/servable.cpp | 2 +- src/test/http_openai_handler_test.cpp | 190 +++++++++++++++++- src/test/ovmsconfig_test.cpp | 8 +- 11 files changed, 254 insertions(+), 15 deletions(-) diff --git a/.bazelrc b/.bazelrc index 71f054bdfb..58e2112ec2 100644 --- a/.bazelrc +++ b/.bazelrc @@ -264,6 +264,7 @@ test:linux --test_env LD_LIBRARY_PATH=/opt/opencv/lib/:/opt/intel/openvino/runti test:linux --test_env OPENVINO_TOKENIZERS_PATH_GENAI=/opt/intel/openvino/runtime/lib/intel64/libopenvino_tokenizers.so test:linux --test_env PYTHONPATH=/opt/intel/openvino/python:/ovms/bazel-bin/src/python/binding test:linux --test_env no_proxy=localhost +test:linux --test_env "OVMS_MEDIA_URL_ALLOW_REDIRECTS=1" # Bazelrc imports ############################################################################################################################ # file below should contain sth like diff --git a/docs/model_server_rest_api_chat.md b/docs/model_server_rest_api_chat.md index 6f01683dc8..4ab8c1cded 100644 --- a/docs/model_server_rest_api_chat.md +++ b/docs/model_server_rest_api_chat.md @@ -112,6 +112,7 @@ curl http://localhost/v3/chat/completions \ "max_completion_tokens": 128 }' ``` +**Note**: using urls in request requires `--allowed_media_domains` parameter described [here](parameters.md) 3) Image from local filesystem: ``` diff --git a/docs/parameters.md b/docs/parameters.md index 9aa06e9507..4271b33dd7 100644 --- a/docs/parameters.md +++ b/docs/parameters.md @@ -20,7 +20,6 @@ | `"low_latency_transformation"` | `bool` | If set to true, model server will apply [low latency transformation](https://docs.openvino.ai/2025/openvino-workflow/running-inference/inference-request/stateful-models/obtaining-stateful-openvino-model.html#lowlatency2-transformation) on model load. | | `"metrics_enable"` | `bool` | Flag enabling [metrics](metrics.md) endpoint on rest_port. | | `"metrics_list"` | `string` | Comma separated list of [metrics](metrics.md). If unset, only default metrics will be enabled.| -| `"allowed_local_media_path"` | `string` | Path to the directory containing images to include in requests. If unset, local filesystem images in requests are not supported.| > **Note** : Specifying config_path is mutually exclusive with putting model parameters in the CLI ([serving multiple models](./starting_server.md)). @@ -57,6 +56,8 @@ Configuration options for the server are defined only via command-line options a | `allowed_methods` | `string` (default: *) | Comma-separated list of allowed methods in CORS requests. | | `allowed_origins` | `string` (default: *) | Comma-separated list of allowed origins in CORS requests. | | `api_key_file` | `string` | Path to the text file with the API key for generative endpoints `/v3/`. The value of first line is used. If not specified, server is using environment variable API_KEY. If not set, requests will not require authorization.| +| `allowed_local_media_path` | `string` | Path to the directory containing images to include in requests. If unset, local filesystem images in requests are not supported.| +| `allowed_media_domains` | `string` | Comma separated list of media domains from which urls can be used as input for LLMs. Set to \"all\" to disable this restriction." ## Config management mode options diff --git a/docs/security_considerations.md b/docs/security_considerations.md index 5c61a70702..44f9ef1f8d 100644 --- a/docs/security_considerations.md +++ b/docs/security_considerations.md @@ -26,6 +26,8 @@ See also: Generative endpoints starting with `/v3`, might be restricted with authorization and API key. It can be set during the server initialization with a parameter `api_key_file` or environment variable `API_KEY`. The `api_key_file` should contain a path to the file containing the value of API key. The content of the file first line is used. If parameter api_key_file and variable API_KEY are not set, the server will not require any authorization. The client should send the API key inside the `Authorization` header as `Bearer `. +OVMS supports multimodal models with image inputs provided as url's. However due to vulnerability to Server-Side Request Forgery (SSRF) attacks, all the url's are restricted by default. To allow fetching image from specific domains use `--allowed_media_domains` parameter described [here](parameters.md). Also, consider setting OVMS_MEDIA_URL_ALLOW_REDIRECTS=0 to prevent HTTP redirects from being followed to bypass domain restrictions. + --- OpenVINO Model Server has a set of mechanisms preventing denial of service attacks from the client applications. They include the following: diff --git a/src/capi_frontend/server_settings.hpp b/src/capi_frontend/server_settings.hpp index 3078aa7d9d..05ed94528c 100644 --- a/src/capi_frontend/server_settings.hpp +++ b/src/capi_frontend/server_settings.hpp @@ -187,6 +187,7 @@ struct ServerSettingsImpl { std::string metricsList; std::string cpuExtensionLibraryPath; std::optional allowedLocalMediaPath; + std::optional> allowedMediaDomains; std::string logLevel = "INFO"; std::string logPath; bool allowCredentials = false; diff --git a/src/cli_parser.cpp b/src/cli_parser.cpp index 9eb6219a0b..7d62d15a4b 100644 --- a/src/cli_parser.cpp +++ b/src/cli_parser.cpp @@ -147,6 +147,10 @@ std::variant> CLIParser::parse(int argc, char* "A path to shared library containing custom CPU layer implementation. Default: empty.", cxxopts::value()->default_value(""), "CPU_EXTENSION") + ("allowed_media_domains", + "Comma separated list of media domains from which urls can be used as input for LLMs. Set to \"all\" to disable this restriction.", + cxxopts::value>(), + "ALLOWED_MEDIA_DOMAINS") ("allowed_local_media_path", "Path to directory that contains multimedia files that can be used as input for LLMs.", cxxopts::value(), @@ -502,6 +506,9 @@ void CLIParser::prepareServer(ServerSettingsImpl& serverSettings) { if (result->count("cpu_extension")) { serverSettings.cpuExtensionLibraryPath = result->operator[]("cpu_extension").as(); } + if (result->count("allowed_media_domains")) { + serverSettings.allowedMediaDomains = result->operator[]("allowed_media_domains").as>(); + } if (result->count("allowed_local_media_path")) { serverSettings.allowedLocalMediaPath = result->operator[]("allowed_local_media_path").as(); } diff --git a/src/llm/apis/openai_completions.cpp b/src/llm/apis/openai_completions.cpp index 3b0c0e2ce1..dea76ab1ee 100644 --- a/src/llm/apis/openai_completions.cpp +++ b/src/llm/apis/openai_completions.cpp @@ -21,6 +21,7 @@ #include "src/port/rapidjson_stringbuffer.hpp" #include "src/port/rapidjson_writer.hpp" #include +#include #include "openai_json_response.hpp" @@ -100,7 +101,6 @@ static size_t appendChunkCallback(void* downloadedChunk, size_t size, size_t nme if (status == CURLE_OK) { \ status = setopt; \ } - static absl::Status downloadImage(const char* url, std::string& image, const int64_t& sizeLimit) { CURL* curl_handle = curl_easy_init(); if (!curl_handle) { @@ -113,7 +113,10 @@ static absl::Status downloadImage(const char* url, std::string& image, const int CURL_SETOPT(curl_easy_setopt(curl_handle, CURLOPT_WRITEFUNCTION, appendChunkCallback)) CURL_SETOPT(curl_easy_setopt(curl_handle, CURLOPT_WRITEDATA, &image)) CURL_SETOPT(curl_easy_setopt(curl_handle, CURLOPT_SSL_OPTIONS, CURLSSLOPT_NATIVE_CA)) - CURL_SETOPT(curl_easy_setopt(curl_handle, CURLOPT_FOLLOWLOCATION, 1L)) + const char* envAllowRedirects = std::getenv("OVMS_MEDIA_URL_ALLOW_REDIRECTS"); + if (envAllowRedirects != nullptr && (std::strcmp(envAllowRedirects, "1") == 0)) { + CURL_SETOPT(curl_easy_setopt(curl_handle, CURLOPT_FOLLOWLOCATION, 1L)) + } CURL_SETOPT(curl_easy_setopt(curl_handle, CURLOPT_MAXFILESIZE, sizeLimit)) if (status != CURLE_OK) { @@ -131,6 +134,34 @@ static absl::Status downloadImage(const char* url, std::string& image, const int return absl::OkStatus(); } +static bool isDomainAllowed(std::vector allowedDomains, const char* url) { + if (allowedDomains.size() == 1 && allowedDomains[0] == "all") { + return true; + } + CURLUcode rc; + CURLU* parsedUrl = curl_url(); + rc = curl_url_set(parsedUrl, CURLUPART_URL, url, 0); + if (rc) { + SPDLOG_LOGGER_DEBUG(llm_calculator_logger, "Parsing url {} failed", url); + return false; + } + char* host; + rc = curl_url_get(parsedUrl, CURLUPART_HOST, &host, 0); + if (rc) { + SPDLOG_LOGGER_DEBUG(llm_calculator_logger, "Parsing url {} hostname failed", url); + return false; + } + bool allowed = false; + for (auto allowedDomain : allowedDomains) { + if (allowedDomain.compare(host) == 0) { + allowed = true; + } + } + curl_free(host); + curl_url_cleanup(parsedUrl); + return allowed; +} + absl::Status OpenAIChatCompletionsHandler::ensureArgumentsInToolCalls(Value& messageObj, bool& jsonChanged) { auto& allocator = doc.GetAllocator(); auto toolCallsIt = messageObj.FindMember("tool_calls"); @@ -159,7 +190,7 @@ absl::Status OpenAIChatCompletionsHandler::ensureArgumentsInToolCalls(Value& mes return absl::OkStatus(); } -absl::Status OpenAIChatCompletionsHandler::parseMessages(std::optional allowedLocalMediaPath) { +absl::Status OpenAIChatCompletionsHandler::parseMessages(std::optional allowedLocalMediaPath, std::optional> allowedMediaDomains) { auto it = doc.FindMember("messages"); if (it == doc.MemberEnd()) return absl::InvalidArgumentError("Messages missing in request"); @@ -237,6 +268,9 @@ absl::Status OpenAIChatCompletionsHandler::parseMessages(std::optional maxTokensLimit, std::optional allowedLocalMediaPath) { +absl::Status OpenAIChatCompletionsHandler::parseChatCompletionsPart(std::optional maxTokensLimit, std::optional allowedLocalMediaPath, std::optional> allowedMediaDomains) { // messages: [{role: content}, {role: content}, ...]; required - auto status = parseMessages(allowedLocalMediaPath); + auto status = parseMessages(allowedLocalMediaPath, allowedMediaDomains); if (status != absl::OkStatus()) { return status; } @@ -791,14 +825,14 @@ void OpenAIChatCompletionsHandler::incrementProcessedTokens(size_t numTokens) { usage.completionTokens += numTokens; } -absl::Status OpenAIChatCompletionsHandler::parseRequest(std::optional maxTokensLimit, uint32_t bestOfLimit, std::optional maxModelLength, std::optional allowedLocalMediaPath) { +absl::Status OpenAIChatCompletionsHandler::parseRequest(std::optional maxTokensLimit, uint32_t bestOfLimit, std::optional maxModelLength, std::optional allowedLocalMediaPath, std::optional> allowedMediaDomains) { absl::Status status = parseCommonPart(maxTokensLimit, bestOfLimit, maxModelLength); if (status != absl::OkStatus()) return status; if (endpoint == Endpoint::COMPLETIONS) status = parseCompletionsPart(); else - status = parseChatCompletionsPart(maxTokensLimit, allowedLocalMediaPath); + status = parseChatCompletionsPart(maxTokensLimit, allowedLocalMediaPath, allowedMediaDomains); return status; } diff --git a/src/llm/apis/openai_completions.hpp b/src/llm/apis/openai_completions.hpp index e3b3a4bd43..7292d99c01 100644 --- a/src/llm/apis/openai_completions.hpp +++ b/src/llm/apis/openai_completions.hpp @@ -72,7 +72,7 @@ class OpenAIChatCompletionsHandler { std::unique_ptr outputParser = nullptr; absl::Status parseCompletionsPart(); - absl::Status parseChatCompletionsPart(std::optional maxTokensLimit, std::optional allowedLocalMediaPath); + absl::Status parseChatCompletionsPart(std::optional maxTokensLimit, std::optional allowedLocalMediaPath, std::optional> allowedMediaDomains); absl::Status parseCommonPart(std::optional maxTokensLimit, uint32_t bestOfLimit, std::optional maxModelLength); ParsedOutput parseOutputIfNeeded(const std::vector& generatedIds); @@ -112,8 +112,8 @@ class OpenAIChatCompletionsHandler { void incrementProcessedTokens(size_t numTokens = 1); - absl::Status parseRequest(std::optional maxTokensLimit, uint32_t bestOfLimit, std::optional maxModelLength, std::optional allowedLocalMediaPath = std::nullopt); - absl::Status parseMessages(std::optional allowedLocalMediaPath = std::nullopt); + absl::Status parseRequest(std::optional maxTokensLimit, uint32_t bestOfLimit, std::optional maxModelLength, std::optional allowedLocalMediaPath = std::nullopt, std::optional> allowedMediaDomains = std::nullopt); + absl::Status parseMessages(std::optional allowedLocalMediaPath = std::nullopt, std::optional> allowedMediaDomains = std::nullopt); absl::Status parseTools(); const bool areToolsAvailable() const; diff --git a/src/llm/visual_language_model/legacy/servable.cpp b/src/llm/visual_language_model/legacy/servable.cpp index 722f57c997..817bb54d23 100644 --- a/src/llm/visual_language_model/legacy/servable.cpp +++ b/src/llm/visual_language_model/legacy/servable.cpp @@ -84,7 +84,7 @@ absl::Status VisualLanguageModelLegacyServable::parseRequest(std::shared_ptrtokenizer); auto& config = ovms::Config::instance(); - auto status = executionContext->apiHandler->parseRequest(getProperties()->maxTokensLimit, getProperties()->bestOfLimit, getProperties()->maxModelLength, config.getServerSettings().allowedLocalMediaPath); + auto status = executionContext->apiHandler->parseRequest(getProperties()->maxTokensLimit, getProperties()->bestOfLimit, getProperties()->maxModelLength, config.getServerSettings().allowedLocalMediaPath, config.getServerSettings().allowedMediaDomains); if (!status.ok()) { SPDLOG_LOGGER_ERROR(llm_calculator_logger, "Failed to parse request: {}", status.message()); return status; diff --git a/src/test/http_openai_handler_test.cpp b/src/test/http_openai_handler_test.cpp index 3218796818..fea0b272c7 100644 --- a/src/test/http_openai_handler_test.cpp +++ b/src/test/http_openai_handler_test.cpp @@ -464,7 +464,44 @@ TEST_F(HttpOpenAIHandlerParsingTest, ParsingMessagesSucceedsUrlHttp) { doc.Parse(json.c_str()); ASSERT_FALSE(doc.HasParseError()); std::shared_ptr apiHandler = std::make_shared(doc, ovms::Endpoint::CHAT_COMPLETIONS, std::chrono::system_clock::now(), *tokenizer); - ASSERT_EQ(apiHandler->parseMessages(), absl::OkStatus()); + std::vector allowedDomains = {"raw.githubusercontent.com"}; + ASSERT_EQ(apiHandler->parseMessages(std::nullopt, allowedDomains), absl::OkStatus()); + const ovms::ImageHistory& imageHistory = apiHandler->getImageHistory(); + ASSERT_EQ(imageHistory.size(), 1); + auto [index, image] = imageHistory[0]; + EXPECT_EQ(index, 0); + EXPECT_EQ(image.get_element_type(), ov::element::u8); + EXPECT_EQ(image.get_size(), 225792); + json = apiHandler->getProcessedJson(); + EXPECT_EQ(json, std::string("{\"model\":\"llama\",\"messages\":[{\"role\":\"user\",\"content\":\"What is in this image?\"}]}")); +} + +TEST_F(HttpOpenAIHandlerParsingTest, ParsingMessagesSucceedsUrlHttpMultipleAllowedDomains) { + std::string json = R"({ + "model": "llama", + "messages": [ + { + "role": "user", + "content": [ + { + "type": "text", + "text": "What is in this image?" + }, + { + "type": "image_url", + "image_url": { + "url": "http://raw.githubusercontent.com/openvinotoolkit/model_server/refs/heads/main/demos/common/static/images/zebra.jpeg" + } + } + ] + } + ] +})"; + doc.Parse(json.c_str()); + ASSERT_FALSE(doc.HasParseError()); + std::shared_ptr apiHandler = std::make_shared(doc, ovms::Endpoint::CHAT_COMPLETIONS, std::chrono::system_clock::now(), *tokenizer); + std::vector allowedDomains = {"raw.githubusercontent.com", "githubusercontent.com", "google.com"}; + ASSERT_EQ(apiHandler->parseMessages(std::nullopt, allowedDomains), absl::OkStatus()); const ovms::ImageHistory& imageHistory = apiHandler->getImageHistory(); ASSERT_EQ(imageHistory.size(), 1); auto [index, image] = imageHistory[0]; @@ -499,7 +536,44 @@ TEST_F(HttpOpenAIHandlerParsingTest, ParsingMessagesSucceedsUrlHttps) { doc.Parse(json.c_str()); ASSERT_FALSE(doc.HasParseError()); std::shared_ptr apiHandler = std::make_shared(doc, ovms::Endpoint::CHAT_COMPLETIONS, std::chrono::system_clock::now(), *tokenizer); - ASSERT_EQ(apiHandler->parseMessages(), absl::OkStatus()); + std::vector allowedDomains = {"raw.githubusercontent.com"}; + ASSERT_EQ(apiHandler->parseMessages(std::nullopt, allowedDomains), absl::OkStatus()); + const ovms::ImageHistory& imageHistory = apiHandler->getImageHistory(); + ASSERT_EQ(imageHistory.size(), 1); + auto [index, image] = imageHistory[0]; + EXPECT_EQ(index, 0); + EXPECT_EQ(image.get_element_type(), ov::element::u8); + EXPECT_EQ(image.get_size(), 225792); + json = apiHandler->getProcessedJson(); + EXPECT_EQ(json, std::string("{\"model\":\"llama\",\"messages\":[{\"role\":\"user\",\"content\":\"What is in this image?\"}]}")); +} + +TEST_F(HttpOpenAIHandlerParsingTest, ParsingMessagesSucceedsUrlHttpsAllowedDomainAll) { + std::string json = R"({ +"model": "llama", +"messages": [ + { + "role": "user", + "content": [ + { + "type": "text", + "text": "What is in this image?" + }, + { + "type": "image_url", + "image_url": { + "url": "https://raw.githubusercontent.com/openvinotoolkit/model_server/refs/heads/main/demos/common/static/images/zebra.jpeg" + } + } + ] + } +] +})"; + doc.Parse(json.c_str()); + ASSERT_FALSE(doc.HasParseError()); + std::shared_ptr apiHandler = std::make_shared(doc, ovms::Endpoint::CHAT_COMPLETIONS, std::chrono::system_clock::now(), *tokenizer); + std::vector allowedDomains = {"all"}; + ASSERT_EQ(apiHandler->parseMessages(std::nullopt, allowedDomains), absl::OkStatus()); const ovms::ImageHistory& imageHistory = apiHandler->getImageHistory(); ASSERT_EQ(imageHistory.size(), 1); auto [index, image] = imageHistory[0]; @@ -572,6 +646,118 @@ TEST_F(HttpOpenAIHandlerParsingTest, ParsingMessagesImageStringWithNoPrefixFails EXPECT_EQ(apiHandler->parseMessages(), absl::InvalidArgumentError("Loading images from local filesystem is disabled.")); } +TEST_F(HttpOpenAIHandlerParsingTest, ParsingMessagesFailsUrlHttpNotAllowedDomain) { + std::string json = R"({ + "model": "llama", + "messages": [ + { + "role": "user", + "content": [ + { + "type": "text", + "text": "What is in this image?" + }, + { + "type": "image_url", + "image_url": { + "url": "http://raw.githubusercontent.com/openvinotoolkit/model_server/refs/heads/main/demos/common/static/images/zebra.jpeg" + } + } + ] + } + ] +})"; + doc.Parse(json.c_str()); + ASSERT_FALSE(doc.HasParseError()); + std::shared_ptr apiHandler = std::make_shared(doc, ovms::Endpoint::CHAT_COMPLETIONS, std::chrono::system_clock::now(), *tokenizer); + std::vector allowedDomains = {"wikipedia.com"}; + ASSERT_EQ(apiHandler->parseMessages(std::nullopt, allowedDomains), absl::InvalidArgumentError("Given url does not match any allowed domain from allowed_media_domains")); +} + +TEST_F(HttpOpenAIHandlerParsingTest, ParsingMessagesFailsUrlMatchAllowedDomainPartially1) { + std::string json = R"({ + "model": "llama", + "messages": [ + { + "role": "user", + "content": [ + { + "type": "text", + "text": "What is in this image?" + }, + { + "type": "image_url", + "image_url": { + "url": "http://raw.githubusercontent.com/openvinotoolkit/model_server/refs/heads/main/demos/common/static/images/zebra.jpeg" + } + } + ] + } + ] +})"; + doc.Parse(json.c_str()); + ASSERT_FALSE(doc.HasParseError()); + std::shared_ptr apiHandler = std::make_shared(doc, ovms::Endpoint::CHAT_COMPLETIONS, std::chrono::system_clock::now(), *tokenizer); + std::vector allowedDomains = {"githubusercontent.com"}; + ASSERT_EQ(apiHandler->parseMessages(std::nullopt, allowedDomains), absl::InvalidArgumentError("Given url does not match any allowed domain from allowed_media_domains")); +} + +TEST_F(HttpOpenAIHandlerParsingTest, ParsingMessagesFailsUrlMatchAllowedDomainPartially2) { + std::string json = R"({ + "model": "llama", + "messages": [ + { + "role": "user", + "content": [ + { + "type": "text", + "text": "What is in this image?" + }, + { + "type": "image_url", + "image_url": { + "url": "http://raw.githubusercontent.com/openvinotoolkit/model_server/refs/heads/main/demos/common/static/images/zebra.jpeg" + } + } + ] + } + ] +})"; + doc.Parse(json.c_str()); + ASSERT_FALSE(doc.HasParseError()); + std::shared_ptr apiHandler = std::make_shared(doc, ovms::Endpoint::CHAT_COMPLETIONS, std::chrono::system_clock::now(), *tokenizer); + std::vector allowedDomains = {"host.raw.githubusercontent.com"}; + ASSERT_EQ(apiHandler->parseMessages(std::nullopt, allowedDomains), absl::InvalidArgumentError("Given url does not match any allowed domain from allowed_media_domains")); +} + +TEST_F(HttpOpenAIHandlerParsingTest, ParsingMessagesFailsRegexNotSupported) { + std::string json = R"({ + "model": "llama", + "messages": [ + { + "role": "user", + "content": [ + { + "type": "text", + "text": "What is in this image?" + }, + { + "type": "image_url", + "image_url": { + "url": "http://raw.githubusercontent.com/openvinotoolkit/model_server/refs/heads/main/demos/common/static/images/zebra.jpeg" + } + } + ] + } + ] +})"; + doc.Parse(json.c_str()); + ASSERT_FALSE(doc.HasParseError()); + std::shared_ptr apiHandler = std::make_shared(doc, ovms::Endpoint::CHAT_COMPLETIONS, std::chrono::system_clock::now(), *tokenizer); + std::vector allowedDomains = {"*githubusercontent.com"}; + ASSERT_EQ(apiHandler->parseMessages(std::nullopt, allowedDomains), absl::InvalidArgumentError("Given url does not match any allowed domain from allowed_media_domains")); +} + TEST_F(HttpOpenAIHandlerParsingTest, ParsingMessagesImageLocalFilesystem) { std::string json = R"({ "model": "llama", diff --git a/src/test/ovmsconfig_test.cpp b/src/test/ovmsconfig_test.cpp index ade6f80db3..9c3d41346e 100644 --- a/src/test/ovmsconfig_test.cpp +++ b/src/test/ovmsconfig_test.cpp @@ -2334,13 +2334,14 @@ TEST(OvmsConfigTest, positiveMulti) { #endif "--cache_dir", "/tmp/model_cache", "--allowed_local_media_path", "/tmp/path", + "--allowed_media_domains", "raw.githubusercontent.com,githubusercontent.com,google.com", "--log_path", "/tmp/log_path", "--log_level", "ERROR", "--grpc_max_threads", "100", "--grpc_memory_quota", "1000000", "--config_path", "/config.json"}; - int arg_count = 44; + int arg_count = 46; ConstructorEnabledConfig config; config.parse(arg_count, n_argv); @@ -2364,6 +2365,11 @@ TEST(OvmsConfigTest, positiveMulti) { EXPECT_EQ(config.cacheDir(), "/tmp/model_cache"); ASSERT_TRUE(config.getServerSettings().allowedLocalMediaPath.has_value()); EXPECT_EQ(config.getServerSettings().allowedLocalMediaPath.value(), "/tmp/path"); + ASSERT_TRUE(config.getServerSettings().allowedMediaDomains.has_value()); + EXPECT_EQ(config.getServerSettings().allowedMediaDomains.value().size(), 3); + EXPECT_EQ(config.getServerSettings().allowedMediaDomains.value()[0], "raw.githubusercontent.com"); + EXPECT_EQ(config.getServerSettings().allowedMediaDomains.value()[1], "githubusercontent.com"); + EXPECT_EQ(config.getServerSettings().allowedMediaDomains.value()[2], "google.com"); EXPECT_EQ(config.logPath(), "/tmp/log_path"); EXPECT_EQ(config.logLevel(), "ERROR"); EXPECT_EQ(config.configPath(), "/config.json"); From 17c7bb523a03be2495d477777c027c8e00e41b25 Mon Sep 17 00:00:00 2001 From: Michal Kulakowski Date: Fri, 23 Jan 2026 14:30:07 +0100 Subject: [PATCH 2/6] fix --- docs/parameters.md | 2 +- docs/security_considerations.md | 2 +- src/capi_frontend/capi.cpp | 33 +++++++++++++++++++++++++++++ src/cli_parser.cpp | 2 +- src/llm/apis/openai_completions.cpp | 7 ++++-- src/ovms.h | 18 ++++++++++++++++ src/test/c_api_tests.cpp | 13 ++++++++++++ 7 files changed, 72 insertions(+), 5 deletions(-) diff --git a/docs/parameters.md b/docs/parameters.md index 4271b33dd7..8d1d53c70e 100644 --- a/docs/parameters.md +++ b/docs/parameters.md @@ -57,7 +57,7 @@ Configuration options for the server are defined only via command-line options a | `allowed_origins` | `string` (default: *) | Comma-separated list of allowed origins in CORS requests. | | `api_key_file` | `string` | Path to the text file with the API key for generative endpoints `/v3/`. The value of first line is used. If not specified, server is using environment variable API_KEY. If not set, requests will not require authorization.| | `allowed_local_media_path` | `string` | Path to the directory containing images to include in requests. If unset, local filesystem images in requests are not supported.| -| `allowed_media_domains` | `string` | Comma separated list of media domains from which urls can be used as input for LLMs. Set to \"all\" to disable this restriction." +| `allowed_media_domains` | `string` | Comma separated list of media domains from which URLs can be used as input for LLMs. Set to \"all\" to disable this restrictions." ## Config management mode options diff --git a/docs/security_considerations.md b/docs/security_considerations.md index 44f9ef1f8d..142a6f82f4 100644 --- a/docs/security_considerations.md +++ b/docs/security_considerations.md @@ -26,7 +26,7 @@ See also: Generative endpoints starting with `/v3`, might be restricted with authorization and API key. It can be set during the server initialization with a parameter `api_key_file` or environment variable `API_KEY`. The `api_key_file` should contain a path to the file containing the value of API key. The content of the file first line is used. If parameter api_key_file and variable API_KEY are not set, the server will not require any authorization. The client should send the API key inside the `Authorization` header as `Bearer `. -OVMS supports multimodal models with image inputs provided as url's. However due to vulnerability to Server-Side Request Forgery (SSRF) attacks, all the url's are restricted by default. To allow fetching image from specific domains use `--allowed_media_domains` parameter described [here](parameters.md). Also, consider setting OVMS_MEDIA_URL_ALLOW_REDIRECTS=0 to prevent HTTP redirects from being followed to bypass domain restrictions. +OVMS supports multimodal models with image inputs provided as URL. However, to prevent Request Forgery (SSRF) attacks, all the URLs are restricted by default. To allow fetching image from specific domains use `--allowed_media_domains` parameter described [here](parameters.md). Also, consider setting OVMS_MEDIA_URL_ALLOW_REDIRECTS=1 to allow HTTP redirects, by default disabled to prevent from bypassing domain restrictions. --- diff --git a/src/capi_frontend/capi.cpp b/src/capi_frontend/capi.cpp index 97694d938d..412920317f 100644 --- a/src/capi_frontend/capi.cpp +++ b/src/capi_frontend/capi.cpp @@ -574,6 +574,39 @@ DLL_PUBLIC OVMS_Status* OVMS_ServerSettingsSetLogPath(OVMS_ServerSettings* setti return nullptr; } +DLL_PUBLIC OVMS_Status* OVMS_ServerSettingsSetAllowedLocalMediaPath(OVMS_ServerSettings* settings, + const char* allowed_local_media_path) { + if (settings == nullptr) { + return reinterpret_cast(new Status(StatusCode::NONEXISTENT_PTR, "server settings")); + } + if (allowed_local_media_path == nullptr) { + return reinterpret_cast(new Status(StatusCode::NONEXISTENT_PTR, "log path")); + } + ovms::ServerSettingsImpl* serverSettings = reinterpret_cast(settings); + serverSettings->allowedLocalMediaPath = allowed_local_media_path; + return nullptr; +} + +DLL_PUBLIC OVMS_Status* OVMS_ServerSettingsSetAllowedMediaDomains(OVMS_ServerSettings* settings, + const char* allowed_media_domains){ + if (settings == nullptr) { + return reinterpret_cast(new Status(StatusCode::NONEXISTENT_PTR, "server settings")); + } + if (allowed_media_domains == nullptr) { + return reinterpret_cast(new Status(StatusCode::NONEXISTENT_PTR, "log path")); + } + std::vector domains; + std::string domain; + std::istringstream ss(allowed_media_domains); + while (std::getline(ss, domain, ',')) + { + domains.push_back(domain); + } + ovms::ServerSettingsImpl* serverSettings = reinterpret_cast(settings); + serverSettings->allowedMediaDomains = domains; + return nullptr; +} + DLL_PUBLIC OVMS_Status* OVMS_ModelsSettingsSetConfigPath(OVMS_ModelsSettings* settings, const char* config_path) { if (settings == nullptr) { diff --git a/src/cli_parser.cpp b/src/cli_parser.cpp index 7d62d15a4b..7393c890ca 100644 --- a/src/cli_parser.cpp +++ b/src/cli_parser.cpp @@ -148,7 +148,7 @@ std::variant> CLIParser::parse(int argc, char* cxxopts::value()->default_value(""), "CPU_EXTENSION") ("allowed_media_domains", - "Comma separated list of media domains from which urls can be used as input for LLMs. Set to \"all\" to disable this restriction.", + "Comma separated list of media domains from which URLs can be used as input for LLMs. Set to \"all\" to disable this restriction.", cxxopts::value>(), "ALLOWED_MEDIA_DOMAINS") ("allowed_local_media_path", diff --git a/src/llm/apis/openai_completions.cpp b/src/llm/apis/openai_completions.cpp index dea76ab1ee..87c03bcc46 100644 --- a/src/llm/apis/openai_completions.cpp +++ b/src/llm/apis/openai_completions.cpp @@ -134,7 +134,7 @@ static absl::Status downloadImage(const char* url, std::string& image, const int return absl::OkStatus(); } -static bool isDomainAllowed(std::vector allowedDomains, const char* url) { +static bool isDomainAllowed(const std::vector& allowedDomains, const char* url) { if (allowedDomains.size() == 1 && allowedDomains[0] == "all") { return true; } @@ -143,18 +143,21 @@ static bool isDomainAllowed(std::vector allowedDomains, const char* rc = curl_url_set(parsedUrl, CURLUPART_URL, url, 0); if (rc) { SPDLOG_LOGGER_DEBUG(llm_calculator_logger, "Parsing url {} failed", url); + curl_url_cleanup(parsedUrl); return false; } char* host; rc = curl_url_get(parsedUrl, CURLUPART_HOST, &host, 0); if (rc) { SPDLOG_LOGGER_DEBUG(llm_calculator_logger, "Parsing url {} hostname failed", url); + curl_url_cleanup(parsedUrl); return false; } bool allowed = false; - for (auto allowedDomain : allowedDomains) { + for (const auto& allowedDomain : allowedDomains) { if (allowedDomain.compare(host) == 0) { allowed = true; + break; } } curl_free(host); diff --git a/src/ovms.h b/src/ovms.h index 8ca223aa58..80ac0bddaf 100644 --- a/src/ovms.h +++ b/src/ovms.h @@ -337,6 +337,24 @@ OVMS_Status* OVMS_ServerSettingsSetLogLevel(OVMS_ServerSettings* settings, OVMS_Status* OVMS_ServerSettingsSetLogPath(OVMS_ServerSettings* settings, const char* log_path); +// Set the server allowed_local_media_path setting. Equivalent of starting server with +// --allowed_local_media_path. +// +// \param settings The server settings object to be set +// \param allowed_local_media_path The value to be set +// \return OVMS_Status object in case of failure +OVMS_Status* OVMS_ServerSettingsSetAllowedLocalMediaPath(OVMS_ServerSettings* settings, + const char* allowed_local_media_path); + +// Set the server allowed_media_domains setting. Equivalent of starting server with +// --allowed_media_domains. +// +// \param settings The server settings object to be set +// \param allowed_media_domains The value to be set as comma separated string +// \return OVMS_Status object in case of failure +OVMS_Status* OVMS_ServerSettingsSetAllowedMediaDomains(OVMS_ServerSettings* settings, + const char* allowed_media_domains); + //// //// OVMS_ModelsSettings //// Options for starting multi model server controlled by config.json file diff --git a/src/test/c_api_tests.cpp b/src/test/c_api_tests.cpp index 10b86d98d8..cf97c6a004 100644 --- a/src/test/c_api_tests.cpp +++ b/src/test/c_api_tests.cpp @@ -131,6 +131,8 @@ TEST(CAPIConfigTest, MultiModelConfiguration) { ASSERT_CAPI_STATUS_NULL(OVMS_ServerSettingsSetLogLevel(_serverSettings, OVMS_LOG_TRACE)); ASSERT_CAPI_STATUS_NOT_NULL_EXPECT_CODE(OVMS_ServerSettingsSetLogLevel(_serverSettings, static_cast(99)), StatusCode::NONEXISTENT_LOG_LEVEL); ASSERT_CAPI_STATUS_NULL(OVMS_ServerSettingsSetLogPath(_serverSettings, getGenericFullPathForTmp("/tmp/logs").c_str())); + ASSERT_CAPI_STATUS_NULL(OVMS_ServerSettingsSetAllowedLocalMediaPath(_serverSettings, getGenericFullPathForTmp("/tmp/path").c_str())); + ASSERT_CAPI_STATUS_NULL(OVMS_ServerSettingsSetAllowedMediaDomains(_serverSettings, "raw.githubusercontent.com,githubusercontent.com,google.com")); ASSERT_CAPI_STATUS_NULL(OVMS_ModelsSettingsSetConfigPath(_modelsSettings, getGenericFullPathForTmp("/tmp/config").c_str())); // check nullptr ASSERT_CAPI_STATUS_NOT_NULL_EXPECT_CODE(OVMS_ServerSettingsSetGrpcPort(nullptr, 5555), StatusCode::NONEXISTENT_PTR); @@ -155,6 +157,10 @@ TEST(CAPIConfigTest, MultiModelConfiguration) { ASSERT_CAPI_STATUS_NOT_NULL_EXPECT_CODE(OVMS_ServerSettingsSetLogLevel(nullptr, OVMS_LOG_TRACE), StatusCode::NONEXISTENT_PTR); ASSERT_CAPI_STATUS_NOT_NULL_EXPECT_CODE(OVMS_ServerSettingsSetLogPath(nullptr, getGenericFullPathForTmp("/tmp/logs").c_str()), StatusCode::NONEXISTENT_PTR); ASSERT_CAPI_STATUS_NOT_NULL_EXPECT_CODE(OVMS_ServerSettingsSetLogPath(_serverSettings, nullptr), StatusCode::NONEXISTENT_PTR); + ASSERT_CAPI_STATUS_NOT_NULL_EXPECT_CODE(OVMS_ServerSettingsSetAllowedLocalMediaPath(nullptr, getGenericFullPathForTmp("/tmp/images").c_str()), StatusCode::NONEXISTENT_PTR); + ASSERT_CAPI_STATUS_NOT_NULL_EXPECT_CODE(OVMS_ServerSettingsSetAllowedLocalMediaPath(_serverSettings, nullptr), StatusCode::NONEXISTENT_PTR); + ASSERT_CAPI_STATUS_NOT_NULL_EXPECT_CODE(OVMS_ServerSettingsSetAllowedMediaDomains(nullptr, "raw.githubusercontent.com,githubusercontent.com,google.com"), StatusCode::NONEXISTENT_PTR); + ASSERT_CAPI_STATUS_NOT_NULL_EXPECT_CODE(OVMS_ServerSettingsSetAllowedMediaDomains(_serverSettings, nullptr), StatusCode::NONEXISTENT_PTR); ASSERT_CAPI_STATUS_NOT_NULL_EXPECT_CODE(OVMS_ModelsSettingsSetConfigPath(nullptr, getGenericFullPathForTmp("/tmp/config").c_str()), StatusCode::NONEXISTENT_PTR); ASSERT_CAPI_STATUS_NOT_NULL_EXPECT_CODE(OVMS_ModelsSettingsSetConfigPath(_modelsSettings, nullptr), StatusCode::NONEXISTENT_PTR); @@ -170,6 +176,13 @@ TEST(CAPIConfigTest, MultiModelConfiguration) { EXPECT_EQ(serverSettings->cpuExtensionLibraryPath, getGenericFullPathForSrcTest("/ovms/src/test")); EXPECT_EQ(serverSettings->logLevel, "TRACE"); EXPECT_EQ(serverSettings->logPath, getGenericFullPathForTmp("/tmp/logs")); + ASSERT_TRUE(serverSettings->allowedLocalMediaPath.has_value()); + EXPECT_EQ(serverSettings->allowedLocalMediaPath.value(), "/tmp/path"); + ASSERT_TRUE(serverSettings->allowedMediaDomains.has_value()); + EXPECT_EQ(serverSettings->allowedMediaDomains.value().size(), 3); + EXPECT_EQ(serverSettings->allowedMediaDomains.value()[0], "raw.githubusercontent.com"); + EXPECT_EQ(serverSettings->allowedMediaDomains.value()[1], "githubusercontent.com"); + EXPECT_EQ(serverSettings->allowedMediaDomains.value()[2], "google.com"); // trace path // not tested since it is not supported in C-API EXPECT_EQ(serverSettings->grpcChannelArguments, "grpcargs"); EXPECT_EQ(serverSettings->grpcMaxThreads, 100); From 57e86126badc5a1cff2feb383b2dda9f14faab92 Mon Sep 17 00:00:00 2001 From: Michal Kulakowski Date: Fri, 23 Jan 2026 14:31:41 +0100 Subject: [PATCH 3/6] fix --- src/capi_frontend/capi.cpp | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/capi_frontend/capi.cpp b/src/capi_frontend/capi.cpp index 412920317f..6bdc385c2a 100644 --- a/src/capi_frontend/capi.cpp +++ b/src/capi_frontend/capi.cpp @@ -588,19 +588,18 @@ DLL_PUBLIC OVMS_Status* OVMS_ServerSettingsSetAllowedLocalMediaPath(OVMS_ServerS } DLL_PUBLIC OVMS_Status* OVMS_ServerSettingsSetAllowedMediaDomains(OVMS_ServerSettings* settings, - const char* allowed_media_domains){ + const char* allowed_media_domains) { if (settings == nullptr) { return reinterpret_cast(new Status(StatusCode::NONEXISTENT_PTR, "server settings")); } if (allowed_media_domains == nullptr) { return reinterpret_cast(new Status(StatusCode::NONEXISTENT_PTR, "log path")); } - std::vector domains; - std::string domain; - std::istringstream ss(allowed_media_domains); - while (std::getline(ss, domain, ',')) - { - domains.push_back(domain); + std::vector domains; + std::string domain; + std::istringstream ss(allowed_media_domains); + while (std::getline(ss, domain, ',')) { + domains.push_back(domain); } ovms::ServerSettingsImpl* serverSettings = reinterpret_cast(settings); serverSettings->allowedMediaDomains = domains; From 05f05caae6ec26d66b8315cc66b8719ab76161f4 Mon Sep 17 00:00:00 2001 From: Michal Kulakowski Date: Fri, 23 Jan 2026 14:56:52 +0100 Subject: [PATCH 4/6] Increase OVMS_API_VERSION_MINOR --- src/ovms.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ovms.h b/src/ovms.h index 80ac0bddaf..a67334e4e2 100644 --- a/src/ovms.h +++ b/src/ovms.h @@ -33,7 +33,7 @@ typedef struct OVMS_ServableMetadata_ OVMS_ServableMetadata; typedef struct OVMS_Metadata_ OVMS_Metadata; #define OVMS_API_VERSION_MAJOR 1 -#define OVMS_API_VERSION_MINOR 2 +#define OVMS_API_VERSION_MINOR 3 // Function to retrieve OVMS API version. // From c502b83129fad655826a34f0fa6229342031839f Mon Sep 17 00:00:00 2001 From: Michal Kulakowski Date: Fri, 23 Jan 2026 15:04:53 +0100 Subject: [PATCH 5/6] review fixes --- demos/continuous_batching/vlm/README.md | 14 +++++++------- docs/parameters.md | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/demos/continuous_batching/vlm/README.md b/demos/continuous_batching/vlm/README.md index 8fb9348cdc..2a391f20dc 100644 --- a/demos/continuous_batching/vlm/README.md +++ b/demos/continuous_batching/vlm/README.md @@ -30,7 +30,7 @@ Select deployment option depending on how you prepared models in the previous st Running this command starts the container with CPU only target device: ```bash mkdir -p models -docker run -d -u $(id -u):$(id -g) --rm -p 8000:8000 -v $(pwd)/models:/models:rw openvino/model_server:latest --rest_port 8000 --source_model OpenVINO/InternVL2-2B-int4-ov --model_repository_path /models --model_name OpenGVLab/InternVL2-2B --task text_generation --pipeline_type VLM +docker run -d -u $(id -u):$(id -g) --rm -p 8000:8000 -v $(pwd)/models:/models:rw openvino/model_server:latest --rest_port 8000 --source_model OpenVINO/InternVL2-2B-int4-ov --model_repository_path /models --model_name OpenGVLab/InternVL2-2B --task text_generation --pipeline_type VLM --allowed_media_domains raw.githubusercontent.com ``` **GPU** @@ -39,7 +39,7 @@ to `docker run` command, use the image with GPU support. It can be applied using the commands below: ```bash mkdir -p models -docker run -d -u $(id -u):$(id -g) --rm -p 8000:8000 --device /dev/dri --group-add=$(stat -c "%g" /dev/dri/render* | head -n 1) -v $(pwd)/models:/models:rw openvino/model_server:latest-gpu --rest_port 8000 --source_model OpenVINO/InternVL2-2B-int4-ov --model_repository_path models --model_name OpenGVLab/InternVL2-2B --task text_generation --target_device GPU --pipeline_type VLM +docker run -d -u $(id -u):$(id -g) --rm -p 8000:8000 --device /dev/dri --group-add=$(stat -c "%g" /dev/dri/render* | head -n 1) -v $(pwd)/models:/models:rw openvino/model_server:latest-gpu --rest_port 8000 --source_model OpenVINO/InternVL2-2B-int4-ov --model_repository_path models --model_name OpenGVLab/InternVL2-2B --task text_generation --target_device GPU --pipeline_type VLM --allowed_media_domains raw.githubusercontent.com ``` ::: @@ -49,11 +49,11 @@ If you run on GPU make sure to have appropriate drivers installed, so the device ```bat mkdir models -ovms --rest_port 8000 --source_model OpenVINO/InternVL2-2B-int4-ov --model_repository_path models --model_name OpenGVLab/InternVL2-2B --task text_generation --pipeline_type VLM --target_device CPU +ovms --rest_port 8000 --source_model OpenVINO/InternVL2-2B-int4-ov --model_repository_path models --model_name OpenGVLab/InternVL2-2B --task text_generation --pipeline_type VLM --target_device CPU --allowed_media_domains raw.githubusercontent.com ``` or ```bat -ovms --rest_port 8000 --source_model OpenVINO/InternVL2-2B-int4-ov --model_repository_path models --model_name OpenGVLab/InternVL2-2B --task text_generation --pipeline_type VLM --target_device GPU +ovms --rest_port 8000 --source_model OpenVINO/InternVL2-2B-int4-ov --model_repository_path models --model_name OpenGVLab/InternVL2-2B --task text_generation --pipeline_type VLM --target_device GPU --allowed_media_domains raw.githubusercontent.com ``` ::: @@ -140,7 +140,7 @@ Select deployment option depending on how you prepared models in the previous st Running this command starts the container with CPU only target device: ```bash -docker run -d --rm -p 8000:8000 -v $(pwd)/models:/models:ro openvino/model_server:latest --rest_port 8000 --model_name OpenGVLab/InternVL2-2B --model_path /models/OpenGVLab/InternVL2-2B +docker run -d --rm -p 8000:8000 -v $(pwd)/models:/models:ro openvino/model_server:latest --rest_port 8000 --model_name OpenGVLab/InternVL2-2B --model_path /models/OpenGVLab/InternVL2-2B --allowed_media_domains raw.githubusercontent.com ``` **GPU** @@ -148,7 +148,7 @@ In case you want to use GPU device to run the generation, add extra docker param to `docker run` command, use the image with GPU support. Export the models with precision matching the GPU capacity and adjust pipeline configuration. It can be applied using the commands below: ```bash -docker run -d --rm -p 8000:8000 --device /dev/dri --group-add=$(stat -c "%g" /dev/dri/render* | head -n 1) -v $(pwd)/models:/models:ro openvino/model_server:latest-gpu --rest_port 8000 --model_name OpenGVLab/InternVL2-2B --model_path /models/OpenGVLab/InternVL2-2B +docker run -d --rm -p 8000:8000 --device /dev/dri --group-add=$(stat -c "%g" /dev/dri/render* | head -n 1) -v $(pwd)/models:/models:ro openvino/model_server:latest-gpu --rest_port 8000 --model_name OpenGVLab/InternVL2-2B --model_path /models/OpenGVLab/InternVL2-2B --allowed_media_domains raw.githubusercontent.com ``` ::: @@ -200,7 +200,7 @@ Let's send a request with text an image in the messages context. ![zebra](../../../demos/common/static/images/zebra.jpeg) :::{dropdown} **Unary call with curl using image url** - +**Note**: using urls in request requires `--allowed_media_domains` parameter described [here](parameters.md) ```bash curl http://localhost:8000/v3/chat/completions -H "Content-Type: application/json" -d "{ \"model\": \"OpenGVLab/InternVL2-2B\", \"messages\":[{\"role\": \"user\", \"content\": [{\"type\": \"text\", \"text\": \"Describe what is one the picture.\"},{\"type\": \"image_url\", \"image_url\": {\"url\": \"http://raw.githubusercontent.com/openvinotoolkit/model_server/refs/heads/releases/2025/3/demos/common/static/images/zebra.jpeg\"}}]}], \"max_completion_tokens\": 100}" diff --git a/docs/parameters.md b/docs/parameters.md index 8d1d53c70e..b0cef276c8 100644 --- a/docs/parameters.md +++ b/docs/parameters.md @@ -57,7 +57,7 @@ Configuration options for the server are defined only via command-line options a | `allowed_origins` | `string` (default: *) | Comma-separated list of allowed origins in CORS requests. | | `api_key_file` | `string` | Path to the text file with the API key for generative endpoints `/v3/`. The value of first line is used. If not specified, server is using environment variable API_KEY. If not set, requests will not require authorization.| | `allowed_local_media_path` | `string` | Path to the directory containing images to include in requests. If unset, local filesystem images in requests are not supported.| -| `allowed_media_domains` | `string` | Comma separated list of media domains from which URLs can be used as input for LLMs. Set to \"all\" to disable this restrictions." +| `allowed_media_domains` | `string` | Comma separated list of media domains from which URLs can be used as input for LLMs. Set to \"all\" to disable this restrictions. If unset, URLs in requests are not supported." ## Config management mode options From ade85ef70d7c5caaa46784831aafce42ec1c403a Mon Sep 17 00:00:00 2001 From: Michal Kulakowski Date: Fri, 23 Jan 2026 16:49:45 +0100 Subject: [PATCH 6/6] fix --- src/llm/apis/openai_completions.cpp | 1 + src/test/c_api_tests.cpp | 2 +- windows_test.bat | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/llm/apis/openai_completions.cpp b/src/llm/apis/openai_completions.cpp index 87c03bcc46..ae2492abd7 100644 --- a/src/llm/apis/openai_completions.cpp +++ b/src/llm/apis/openai_completions.cpp @@ -115,6 +115,7 @@ static absl::Status downloadImage(const char* url, std::string& image, const int CURL_SETOPT(curl_easy_setopt(curl_handle, CURLOPT_SSL_OPTIONS, CURLSSLOPT_NATIVE_CA)) const char* envAllowRedirects = std::getenv("OVMS_MEDIA_URL_ALLOW_REDIRECTS"); if (envAllowRedirects != nullptr && (std::strcmp(envAllowRedirects, "1") == 0)) { + SPDLOG_LOGGER_TRACE(llm_calculator_logger, "URL redirects allowed"); CURL_SETOPT(curl_easy_setopt(curl_handle, CURLOPT_FOLLOWLOCATION, 1L)) } CURL_SETOPT(curl_easy_setopt(curl_handle, CURLOPT_MAXFILESIZE, sizeLimit)) diff --git a/src/test/c_api_tests.cpp b/src/test/c_api_tests.cpp index cf97c6a004..bff2ddc415 100644 --- a/src/test/c_api_tests.cpp +++ b/src/test/c_api_tests.cpp @@ -177,7 +177,7 @@ TEST(CAPIConfigTest, MultiModelConfiguration) { EXPECT_EQ(serverSettings->logLevel, "TRACE"); EXPECT_EQ(serverSettings->logPath, getGenericFullPathForTmp("/tmp/logs")); ASSERT_TRUE(serverSettings->allowedLocalMediaPath.has_value()); - EXPECT_EQ(serverSettings->allowedLocalMediaPath.value(), "/tmp/path"); + EXPECT_EQ(serverSettings->allowedLocalMediaPath.value(), getGenericFullPathForTmp("/tmp/path")); ASSERT_TRUE(serverSettings->allowedMediaDomains.has_value()); EXPECT_EQ(serverSettings->allowedMediaDomains.value().size(), 3); EXPECT_EQ(serverSettings->allowedMediaDomains.value()[0], "raw.githubusercontent.com"); diff --git a/windows_test.bat b/windows_test.bat index 5dbd47bc17..592b9ad67b 100644 --- a/windows_test.bat +++ b/windows_test.bat @@ -27,6 +27,7 @@ IF "%~1"=="" ( set "bazelStartupCmd=--output_user_root=!BAZEL_SHORT_PATH!" set "openvino_dir=!BAZEL_SHORT_PATH!/openvino/runtime/cmake" +set "OVMS_MEDIA_URL_ALLOW_REDIRECTS=1" IF "%~2"=="--with_python" ( set "bazelBuildArgs=--config=win_mp_on_py_on --action_env OpenVINO_DIR=%openvino_dir%"