From 2a205ef3be73865e9372251412265066186c5681 Mon Sep 17 00:00:00 2001 From: adrianlizarraga Date: Wed, 20 May 2026 21:41:00 -0700 Subject: [PATCH 1/4] [OpenVINO EP] Validate that EPContext binary path is within model context directory --- .../openvino/onnx_ctx_model_helper.cc | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/onnxruntime/core/providers/openvino/onnx_ctx_model_helper.cc b/onnxruntime/core/providers/openvino/onnx_ctx_model_helper.cc index 80ce4de4a6a19..44cabb5b04737 100644 --- a/onnxruntime/core/providers/openvino/onnx_ctx_model_helper.cc +++ b/onnxruntime/core/providers/openvino/onnx_ctx_model_helper.cc @@ -5,6 +5,7 @@ #include #include #include +#include #include "core/providers/openvino/onnx_ctx_model_helper.h" #include "core/providers/openvino/backend_utils.h" @@ -12,6 +13,24 @@ namespace onnxruntime { namespace openvino_ep { +namespace { + +bool IsAbsolutePath(const std::string& path_string) { + auto path = std::filesystem::path(path_string); + return path.is_absolute(); +} + +bool IsRelativePathToParentPath(const std::string& path_string) { + auto path = std::filesystem::path(path_string); + auto normalized = path.lexically_normal().string(); + if (normalized.find("..") != std::string::npos) { + return true; + } + return false; +} + +} // namespace + EPCtxHandler::EPCtxHandler(std::string ov_sdk_version, const logging::Logger& logger, std::shared_ptr shared_context_manager) : openvino_sdk_version_(std::move(ov_sdk_version)), logger_(logger), shared_context_manager_(std::move(shared_context_manager)) { ORT_ENFORCE(shared_context_manager_ != nullptr, "SharedContextManager pointer is null in EPCtxHandler constructor."); @@ -114,6 +133,11 @@ std::unique_ptr EPCtxHandler::GetModelBlobStream(const std::fi if (blob_filepath.empty() && !graph_viewer.ModelPath().empty()) { blob_filepath = graph_viewer.ModelPath(); } + // ep_cache_context must be a relative path within the context model directory. + ORT_ENFORCE(!IsAbsolutePath(ep_cache_context), + "ep_cache_context must be a relative path, but got absolute path: ", ep_cache_context); + ORT_ENFORCE(!IsRelativePathToParentPath(ep_cache_context), + "ep_cache_context must not contain '..'; it cannot point outside the model directory."); blob_filepath = blob_filepath.parent_path() / ep_cache_context; ORT_ENFORCE(std::filesystem::exists(blob_filepath), "Blob file not found: ", blob_filepath.string()); result.reset((std::istream*)new std::ifstream(blob_filepath, std::ios_base::binary | std::ios_base::in)); @@ -242,6 +266,11 @@ std::shared_ptr EPCtxHandler::Initialize(const std::vectorDeserialize(ss); } } else { + // ep_cache_context must be a relative path within the context model directory. + ORT_ENFORCE(!IsAbsolutePath(ep_cache_context), + "ep_cache_context must be a relative path, but got absolute path: ", ep_cache_context); + ORT_ENFORCE(!IsRelativePathToParentPath(ep_cache_context), + "ep_cache_context must not contain '..'; it cannot point outside the model directory."); std::filesystem::path ep_context_path = session_context.GetOutputModelPath().parent_path() / ep_cache_context; if (ep_context_path.extension() != ".xml") { shared_context = shared_context_manager_->GetOrCreateSharedContext(ep_context_path); From 810d3503379ecfbde8a16f611a7bdaaea43849a6 Mon Sep 17 00:00:00 2001 From: Adrian Lizarraga Date: Wed, 20 May 2026 21:49:49 -0700 Subject: [PATCH 2/4] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../core/providers/openvino/onnx_ctx_model_helper.cc | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/onnxruntime/core/providers/openvino/onnx_ctx_model_helper.cc b/onnxruntime/core/providers/openvino/onnx_ctx_model_helper.cc index 44cabb5b04737..b6fd0c5c6ef13 100644 --- a/onnxruntime/core/providers/openvino/onnx_ctx_model_helper.cc +++ b/onnxruntime/core/providers/openvino/onnx_ctx_model_helper.cc @@ -17,14 +17,15 @@ namespace { bool IsAbsolutePath(const std::string& path_string) { auto path = std::filesystem::path(path_string); - return path.is_absolute(); + return path.has_root_path(); } bool IsRelativePathToParentPath(const std::string& path_string) { - auto path = std::filesystem::path(path_string); - auto normalized = path.lexically_normal().string(); - if (normalized.find("..") != std::string::npos) { - return true; + auto normalized = std::filesystem::path(path_string).lexically_normal(); + for (const auto& component : normalized) { + if (component == "..") { + return true; + } } return false; } From 4c83c661b18c3d55049f0812a78e685e8907cf24 Mon Sep 17 00:00:00 2001 From: adrianlizarraga Date: Wed, 20 May 2026 23:37:46 -0700 Subject: [PATCH 3/4] Check for symlinks --- .../openvino/onnx_ctx_model_helper.cc | 79 ++++++++++++++----- 1 file changed, 60 insertions(+), 19 deletions(-) diff --git a/onnxruntime/core/providers/openvino/onnx_ctx_model_helper.cc b/onnxruntime/core/providers/openvino/onnx_ctx_model_helper.cc index 44cabb5b04737..d36f54e3a3daa 100644 --- a/onnxruntime/core/providers/openvino/onnx_ctx_model_helper.cc +++ b/onnxruntime/core/providers/openvino/onnx_ctx_model_helper.cc @@ -15,18 +15,67 @@ namespace openvino_ep { namespace { -bool IsAbsolutePath(const std::string& path_string) { - auto path = std::filesystem::path(path_string); - return path.is_absolute(); +// Checks whether `path` starts with the given directory prefix (component-wise). +bool HasPathComponentPrefix(const std::filesystem::path& prefix, const std::filesystem::path& path) { + auto [prefix_end, path_it] = std::mismatch(prefix.begin(), prefix.end(), path.begin(), path.end()); + return prefix_end == prefix.end(); } -bool IsRelativePathToParentPath(const std::string& path_string) { - auto path = std::filesystem::path(path_string); - auto normalized = path.lexically_normal().string(); - if (normalized.find("..") != std::string::npos) { - return true; +// Validates that ep_cache_context resolves to a path within the model directory. +// Resolves symlinks via weakly_canonical to prevent traversal attacks. +Status ValidateEpCacheContextPath(const std::filesystem::path& model_path, + const std::string& ep_cache_context) { + std::filesystem::path cache_path(ep_cache_context); + + // Reject absolute paths (covers both Unix '/' and Windows 'C:\' style). + if (!cache_path.root_path().empty()) { + return ORT_MAKE_STATUS(ONNXRUNTIME, EP_FAIL, + "ep_cache_context must be a relative path, but got: ", ep_cache_context); } - return false; + + // Determine model directory. + std::filesystem::path model_dir = model_path.empty() || model_path.parent_path().empty() + ? std::filesystem::path{"."} + : model_path.parent_path(); + + // Resolve both paths to their weakly canonical forms (resolves symlinks for existing + // path components, normalizes the rest). + std::error_code ec; + auto model_dir_canonical = std::filesystem::weakly_canonical(model_dir, ec); + if (ec) { + return ORT_MAKE_STATUS(ONNXRUNTIME, EP_FAIL, + "Failed to resolve model directory: ", model_dir.string(), " - ", ec.message()); + } + + auto cache_path_canonical = std::filesystem::weakly_canonical(model_dir / cache_path, ec); + if (ec) { + return ORT_MAKE_STATUS(ONNXRUNTIME, EP_FAIL, + "Failed to resolve ep_cache_context path: ", (model_dir / cache_path).string(), + " - ", ec.message()); + } + + // Verify the resolved cache path is contained within the model directory. + if (HasPathComponentPrefix(model_dir_canonical, cache_path_canonical)) { + return Status::OK(); + } + + // The model file itself may be a symlink (e.g., Hugging Face Hub cache). + // Check against the real model directory after resolving all symlinks. + if (!model_path.empty() && std::filesystem::is_symlink(model_path, ec) && !ec) { + auto real_model_path = std::filesystem::weakly_canonical(model_path, ec); + if (!ec) { + auto real_model_dir = real_model_path.parent_path(); + if (HasPathComponentPrefix(real_model_dir, cache_path_canonical)) { + return Status::OK(); + } + } + } + + return ORT_MAKE_STATUS(ONNXRUNTIME, EP_FAIL, + "ep_cache_context path escapes model directory. ", + "ep_cache_context: ", ep_cache_context, + ", resolved path: ", cache_path_canonical.string(), + ", model directory: ", model_dir_canonical.string()); } } // namespace @@ -133,11 +182,7 @@ std::unique_ptr EPCtxHandler::GetModelBlobStream(const std::fi if (blob_filepath.empty() && !graph_viewer.ModelPath().empty()) { blob_filepath = graph_viewer.ModelPath(); } - // ep_cache_context must be a relative path within the context model directory. - ORT_ENFORCE(!IsAbsolutePath(ep_cache_context), - "ep_cache_context must be a relative path, but got absolute path: ", ep_cache_context); - ORT_ENFORCE(!IsRelativePathToParentPath(ep_cache_context), - "ep_cache_context must not contain '..'; it cannot point outside the model directory."); + ORT_THROW_IF_ERROR(ValidateEpCacheContextPath(blob_filepath, ep_cache_context)); blob_filepath = blob_filepath.parent_path() / ep_cache_context; ORT_ENFORCE(std::filesystem::exists(blob_filepath), "Blob file not found: ", blob_filepath.string()); result.reset((std::istream*)new std::ifstream(blob_filepath, std::ios_base::binary | std::ios_base::in)); @@ -266,11 +311,7 @@ std::shared_ptr EPCtxHandler::Initialize(const std::vectorDeserialize(ss); } } else { - // ep_cache_context must be a relative path within the context model directory. - ORT_ENFORCE(!IsAbsolutePath(ep_cache_context), - "ep_cache_context must be a relative path, but got absolute path: ", ep_cache_context); - ORT_ENFORCE(!IsRelativePathToParentPath(ep_cache_context), - "ep_cache_context must not contain '..'; it cannot point outside the model directory."); + ORT_THROW_IF_ERROR(ValidateEpCacheContextPath(session_context.GetOutputModelPath(), ep_cache_context)); std::filesystem::path ep_context_path = session_context.GetOutputModelPath().parent_path() / ep_cache_context; if (ep_context_path.extension() != ".xml") { shared_context = shared_context_manager_->GetOrCreateSharedContext(ep_context_path); From c763f28cd9c030b5f32370ef78b781543935792c Mon Sep 17 00:00:00 2001 From: adrianlizarraga Date: Wed, 20 May 2026 23:48:12 -0700 Subject: [PATCH 4/4] Reject empty path --- onnxruntime/core/providers/openvino/onnx_ctx_model_helper.cc | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/onnxruntime/core/providers/openvino/onnx_ctx_model_helper.cc b/onnxruntime/core/providers/openvino/onnx_ctx_model_helper.cc index d36f54e3a3daa..662222bead3a8 100644 --- a/onnxruntime/core/providers/openvino/onnx_ctx_model_helper.cc +++ b/onnxruntime/core/providers/openvino/onnx_ctx_model_helper.cc @@ -25,6 +25,10 @@ bool HasPathComponentPrefix(const std::filesystem::path& prefix, const std::file // Resolves symlinks via weakly_canonical to prevent traversal attacks. Status ValidateEpCacheContextPath(const std::filesystem::path& model_path, const std::string& ep_cache_context) { + if (ep_cache_context.empty()) { + return ORT_MAKE_STATUS(ONNXRUNTIME, EP_FAIL, "ep_cache_context must not be empty"); + } + std::filesystem::path cache_path(ep_cache_context); // Reject absolute paths (covers both Unix '/' and Windows 'C:\' style).