From c217d84833d57a0c003d20148b0ad8b91b4177f6 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Thu, 7 May 2026 20:46:22 +0100 Subject: [PATCH 1/4] fix(python): percent-encode OpenAPI path parameter values to prevent route traversal Apply urllib.parse.quote(safe='') to path parameter values in RestApiOperation.build_path() before URL substitution. This encodes /, ?, #, %, and spaces, preventing attackers from injecting path separators or dot-segments via LLM-supplied parameter values. Addresses issue 115044 (Sev3). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../models/rest_api_operation.py | 4 +-- .../openapi_plugin/test_sk_openapi.py | 28 +++++++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/python/semantic_kernel/connectors/openapi_plugin/models/rest_api_operation.py b/python/semantic_kernel/connectors/openapi_plugin/models/rest_api_operation.py index 7963c55883e8..570a4352892c 100644 --- a/python/semantic_kernel/connectors/openapi_plugin/models/rest_api_operation.py +++ b/python/semantic_kernel/connectors/openapi_plugin/models/rest_api_operation.py @@ -2,7 +2,7 @@ import re from typing import Any, Final -from urllib.parse import ParseResult, ParseResultBytes, urlencode, urljoin, urlparse, urlunparse +from urllib.parse import ParseResult, ParseResultBytes, quote, urlencode, urljoin, urlparse, urlunparse from semantic_kernel.connectors.openapi_plugin.models.rest_api_expected_response import ( RestApiExpectedResponse, @@ -288,7 +288,7 @@ def build_path(self, path_template: str, arguments: dict[str, Any]) -> str: f"required parameter of the operation - `{self.id}`." ) continue - path_template = path_template.replace(f"{{{parameter.name}}}", str(argument)) + path_template = path_template.replace(f"{{{parameter.name}}}", quote(str(argument), safe="")) return path_template def build_query_string(self, arguments: dict[str, Any]) -> str: diff --git a/python/tests/unit/connectors/openapi_plugin/test_sk_openapi.py b/python/tests/unit/connectors/openapi_plugin/test_sk_openapi.py index 1d25486b5a86..916d9b155b21 100644 --- a/python/tests/unit/connectors/openapi_plugin/test_sk_openapi.py +++ b/python/tests/unit/connectors/openapi_plugin/test_sk_openapi.py @@ -412,6 +412,34 @@ def test_build_path_with_optional_and_required_parameters(): assert operation.build_path(operation.path, arguments) == expected_path +def test_build_path_encodes_special_characters(): + parameters = [RestApiParameter(name="id", type="string", location=RestApiParameterLocation.PATH, is_required=True)] + operation = RestApiOperation( + id="test", method="GET", servers=["https://example.com/"], path="/resource/{id}", params=parameters + ) + # Characters like /, ?, #, and spaces must be percent-encoded to prevent traversal + arguments = {"id": "foo/bar?q=1#frag data"} + result = operation.build_path(operation.path, arguments) + encoded_part = result.split("/resource/")[1] + assert "/" not in encoded_part + assert "?" not in encoded_part + assert "#" not in encoded_part + assert " " not in encoded_part + # Python's quote(safe="") encodes all except unreserved chars (letters, digits, _, ., -, ~) + assert result == "/resource/foo%2Fbar%3Fq%3D1%23frag%20data" + + +def test_build_path_prevents_path_traversal(): + parameters = [RestApiParameter(name="id", type="string", location=RestApiParameterLocation.PATH, is_required=True)] + operation = RestApiOperation( + id="test", method="GET", servers=["https://example.com/"], path="/resource/{id}", params=parameters + ) + arguments = {"id": "../../admin"} + result = operation.build_path(operation.path, arguments) + # The slashes must be encoded so ../../admin becomes a single path segment, not a traversal + assert result == "/resource/..%2F..%2Fadmin" + + def test_build_query_string_with_required_parameter(): parameters = [ RestApiParameter(name="query", type="string", location=RestApiParameterLocation.QUERY, is_required=True) From 34f9509dc664a5b7f30b4e53db8f2f1820dae9be Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Fri, 8 May 2026 10:00:39 +0100 Subject: [PATCH 2/4] test: add coverage for double-encoding and unicode path parameters Add tests for pre-encoded input (double-encoding by design) and non-ASCII/unicode characters (UTF-8 percent-encoding) in build_path(). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../openapi_plugin/test_sk_openapi.py | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/python/tests/unit/connectors/openapi_plugin/test_sk_openapi.py b/python/tests/unit/connectors/openapi_plugin/test_sk_openapi.py index 916d9b155b21..2dd2488fc506 100644 --- a/python/tests/unit/connectors/openapi_plugin/test_sk_openapi.py +++ b/python/tests/unit/connectors/openapi_plugin/test_sk_openapi.py @@ -440,6 +440,28 @@ def test_build_path_prevents_path_traversal(): assert result == "/resource/..%2F..%2Fadmin" +def test_build_path_double_encodes_pre_encoded_values(): + """Arguments must be raw/unencoded values. Pre-encoded values are double-encoded by design.""" + parameters = [RestApiParameter(name="id", type="string", location=RestApiParameterLocation.PATH, is_required=True)] + operation = RestApiOperation( + id="test", method="GET", servers=["https://example.com/"], path="/resource/{id}", params=parameters + ) + arguments = {"id": "hello%2Fworld"} + result = operation.build_path(operation.path, arguments) + # %2F in input becomes %252F — the % is encoded, preventing decode-based bypass + assert result == "/resource/hello%252Fworld" + + +def test_build_path_encodes_unicode_characters(): + parameters = [RestApiParameter(name="id", type="string", location=RestApiParameterLocation.PATH, is_required=True)] + operation = RestApiOperation( + id="test", method="GET", servers=["https://example.com/"], path="/resource/{id}", params=parameters + ) + arguments = {"id": "café résumé"} + result = operation.build_path(operation.path, arguments) + assert result == "/resource/caf%C3%A9%20r%C3%A9sum%C3%A9" + + def test_build_query_string_with_required_parameter(): parameters = [ RestApiParameter(name="query", type="string", location=RestApiParameterLocation.QUERY, is_required=True) From c4c1a3bb04ad1ff128d21ce21da8504c61934dc7 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Mon, 11 May 2026 10:52:36 +0100 Subject: [PATCH 3/4] fix(python): pin azure-search-documents < 12.0.0 to fix CI Version 12.0.0 removed the internal _endpoint attribute from SearchIndexClient, breaking AzureAISearchCollection initialization. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- python/pyproject.toml | 2 +- python/uv.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/python/pyproject.toml b/python/pyproject.toml index c3044451cb76..2b85caa4ca4e 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -73,7 +73,7 @@ aws = [ azure = [ "azure-ai-inference >= 1.0.0b6", "azure-core-tracing-opentelemetry >= 1.0.0b11", - "azure-search-documents >= 11.6.0b4", + "azure-search-documents >= 11.6.0b4, < 12.0.0", "azure-cosmos ~= 4.7" ] chroma = [ diff --git a/python/uv.lock b/python/uv.lock index 430794738a60..10b500f8c457 100644 --- a/python/uv.lock +++ b/python/uv.lock @@ -6480,7 +6480,7 @@ requires-dist = [ { name = "azure-core-tracing-opentelemetry", marker = "extra == 'azure'", specifier = ">=1.0.0b11" }, { name = "azure-cosmos", marker = "extra == 'azure'", specifier = "~=4.7" }, { name = "azure-identity", specifier = ">=1.13" }, - { name = "azure-search-documents", marker = "extra == 'azure'", specifier = ">=11.6.0b4" }, + { name = "azure-search-documents", marker = "extra == 'azure'", specifier = ">=11.6.0b4,<12.0.0" }, { name = "boto3", marker = "extra == 'aws'", specifier = ">=1.36.4,<1.41.0" }, { name = "chromadb", marker = "extra == 'chroma'", specifier = ">=0.5,<1.4" }, { name = "cloudevents", specifier = "~=1.0" }, From b1767f4dcd590923e53a6ed591e30d037ec1d587 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Mon, 11 May 2026 11:08:29 +0100 Subject: [PATCH 4/4] fix(python): pin onnxruntime < 1.26.0 (no macOS ARM64 wheel) onnxruntime 1.26.0 does not ship a macOS ARM64 wheel, breaking CI on macos-latest runners. See: https://github.com/microsoft/onnxruntime/issues/28441 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- python/pyproject.toml | 4 +++- python/uv.lock | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/python/pyproject.toml b/python/pyproject.toml index 2b85caa4ca4e..3383b6db5f1d 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -118,7 +118,9 @@ ollama = [ onnx = [ # onnxruntime>=1.24.0 dropped Python 3.10 support; pin to last compatible version for 3.10. "onnxruntime==1.22.1; python_version == '3.10'", - "onnxruntime>=1.24.3; python_version > '3.10'", + # onnxruntime 1.26.0 has no macOS ARM64 wheel; pin to last compatible version. + # https://github.com/microsoft/onnxruntime/issues/28441 + "onnxruntime>=1.24.3, <1.26.0; python_version > '3.10'", "onnxruntime-genai==0.9.0" ] oracledb = [ diff --git a/python/uv.lock b/python/uv.lock index 10b500f8c457..d1093258d222 100644 --- a/python/uv.lock +++ b/python/uv.lock @@ -6502,7 +6502,7 @@ requires-dist = [ { name = "numpy", marker = "python_full_version >= '3.12'", specifier = ">=1.26.0" }, { name = "ollama", marker = "extra == 'ollama'", specifier = "~=0.4" }, { name = "onnxruntime", marker = "python_full_version == '3.10.*' and extra == 'onnx'", specifier = "==1.22.1" }, - { name = "onnxruntime", marker = "python_full_version >= '3.11' and extra == 'onnx'", specifier = ">=1.24.3" }, + { name = "onnxruntime", marker = "python_full_version >= '3.11' and extra == 'onnx'", specifier = ">=1.24.3,<1.26.0" }, { name = "onnxruntime-genai", marker = "extra == 'onnx'", specifier = "==0.9.0" }, { name = "openai", specifier = ">=2.0.0" }, { name = "openapi-core", specifier = ">=0.18,<0.20" },