|
3 | 3 |
|
4 | 4 | #include <array> |
5 | 5 | #include <filesystem> |
| 6 | +#include <fstream> |
6 | 7 | #include <string> |
7 | 8 | #include <unordered_map> |
8 | 9 | #include <vector> |
@@ -135,6 +136,78 @@ TEST_F(OVEPEPContextOVIRTests, RunEpCtxOvirModel) { |
135 | 136 | RunAndValidate(session); |
136 | 137 | } |
137 | 138 |
|
| 139 | +// Negative / security test: an OVIR-encapsulated EP context model whose |
| 140 | +// "ep_cache_context" attribute points outside the model directory via "../" |
| 141 | +// traversal (e.g. "../../../etc/evil.xml") must be rejected at session-creation |
| 142 | +// time rather than silently reading an arbitrary file off disk. |
| 143 | + |
| 144 | +TEST_F(OVEPEPContextOVIRTests, RejectsEpCacheContextPathTraversal) { |
| 145 | + ASSERT_TRUE(std::filesystem::exists(kOvirModelPath)) |
| 146 | + << "Missing OVIR EP context model. Expected testdata/mul_1_ep_ctx_ovir.onnx " |
| 147 | + "(with sibling .xml and .bin files)."; |
| 148 | + |
| 149 | + // Load the known-good OVIR EP context model and rewrite its EPContext node so |
| 150 | + // that ep_cache_context escapes the model directory. |
| 151 | + ONNX_NAMESPACE::ModelProto model_proto; |
| 152 | + ASSERT_STATUS_OK(Model::Load(kOvirModelPath, model_proto)); |
| 153 | + |
| 154 | + // Malicious relative path that escapes the model directory. The ".xml" |
| 155 | + // extension routes validation through the OVIR ".xml" branch in |
| 156 | + // EPCtxHandler::Initialize() (validated against the input model's directory), |
| 157 | + // and "evil.xml" matches the "evil.onnx" output stem below so the node is also |
| 158 | + // recognized as OVIR-encapsulated. |
| 159 | + const std::string malicious_xml_path = "../../../etc/evil.xml"; |
| 160 | + |
| 161 | + bool patched = false; |
| 162 | + for (auto& node : *model_proto.mutable_graph()->mutable_node()) { |
| 163 | + if (node.op_type() != "EPContext") { |
| 164 | + continue; |
| 165 | + } |
| 166 | + for (auto& attr : *node.mutable_attribute()) { |
| 167 | + if (attr.name() == "embed_mode") { |
| 168 | + attr.set_i(0); // force non-embed so the path (not an inline blob) is validated |
| 169 | + } else if (attr.name() == "ep_cache_context") { |
| 170 | + attr.set_s(malicious_xml_path); |
| 171 | + patched = true; |
| 172 | + } |
| 173 | + } |
| 174 | + } |
| 175 | + ASSERT_TRUE(patched) << "Test model did not contain an EPContext ep_cache_context attribute to patch."; |
| 176 | + |
| 177 | + // Write the tampered model to a dedicated subfolder. The malicious ".xml" is |
| 178 | + // intentionally never created on disk: validation must reject the path before |
| 179 | + // any attempt to read it. |
| 180 | + const std::filesystem::path out_dir = std::filesystem::path("testdata") / "ovir_epctx_path_traversal"; |
| 181 | + std::filesystem::remove_all(out_dir); |
| 182 | + std::filesystem::create_directories(out_dir); |
| 183 | + const std::filesystem::path malicious_model = out_dir / "evil.onnx"; |
| 184 | + { |
| 185 | + std::ofstream ofs(malicious_model, std::ios::binary); |
| 186 | + ASSERT_TRUE(ofs.is_open()) << "Failed to open " << malicious_model; |
| 187 | + ASSERT_TRUE(model_proto.SerializeToOstream(&ofs)) << "Failed to serialize tampered model."; |
| 188 | + } |
| 189 | + |
| 190 | + Ort::SessionOptions session_options; |
| 191 | + std::unordered_map<std::string, std::string> ov_options = {{"device_type", kDevice}}; |
| 192 | + session_options.AppendExecutionProvider_OpenVINO_V2(ov_options); |
| 193 | + |
| 194 | + bool threw = false; |
| 195 | + std::string error_message; |
| 196 | + try { |
| 197 | + Ort::Session session(*ort_env, malicious_model.c_str(), session_options); |
| 198 | + } catch (const Ort::Exception& ex) { |
| 199 | + threw = true; |
| 200 | + error_message = ex.what(); |
| 201 | + } |
| 202 | + |
| 203 | + std::filesystem::remove_all(out_dir); |
| 204 | + |
| 205 | + ASSERT_TRUE(threw) |
| 206 | + << "Session creation should have rejected the path-traversal ep_cache_context, but it succeeded."; |
| 207 | + EXPECT_THAT(error_message, ::testing::HasSubstr("escapes model directory")) |
| 208 | + << "Expected a path-escape rejection. Actual error: " << error_message; |
| 209 | +} |
| 210 | + |
138 | 211 | // Generates an EP context model from the OVIR-encapsulated source model and |
139 | 212 | // then loads + runs the generated model, covering both EP context embed modes: |
140 | 213 | // embed_mode = 1: the compiled context is serialized INLINE into the .onnx. |
|
0 commit comments