Security Model for External Data in onnx-irΒΆ
This document describes the threat model, implemented defenses, known
limitations, and design rationale for how onnx-ir handles external tensor
data (the ExternalTensor class).
Threat modelΒΆ
ONNX models can reference external data files via relative paths stored in the
location field of a TensorProto. A malicious model could abuse this to
read arbitrary files on the host. The main attack vectors are:
Vector |
Example |
|---|---|
Path traversal |
|
Absolute path |
|
Symlink escape |
|
Hardlink smuggling |
Hard-linking a sensitive file into the model directory |
Implemented defensesΒΆ
ExternalTensor._check_path_containment() enforces a three-layer check
whenever a non-empty base_dir is set:
Layer |
What it does |
|---|---|
1. String-based containment |
Normalizes the path (without resolving symlinks) and verifies it stays within |
2. Realpath containment |
Resolves all symlinks via |
3. Hardlink detection |
Rejects files with more than one hard link ( |
All three checks run at load time β when numpy(), tofile(), or
tobytes() (indirectly, via _load()) is called β not at construction time.
This allows safe deserialization of untrusted protos without triggering I/O.
save(..., external_data=...), external_data.unload_from_model(), and
save_safetensors() also validate external source paths before writing any data.
The planned external data file paths are checked before the writer publishes any
files as well, so an output symlink cannot leave the saved model pointing outside
its directory.
An existing attribute reference is preserved only if it passes the same policy
relative to the destination model directory. If a readable source cannot be
referenced safely from the destination, its bytes are streamed into the new
external data file instead. File identity alone does not establish that a
destination reference is permitted.
base_dir="" bypass (by design)ΒΆ
When base_dir is empty (the default), all security checks are skipped.
This is intentional for two reasons:
Programmatic construction β when a developer creates an
ExternalTensorin code, they control the paths directly and do not need containment checks.Deserialization safety β the IR deserializer may create
ExternalTensorobjects before abase_diris known. Containment is only meaningful when the caller setsbase_dirto the modelβs directory.
If you are loading an untrusted model, always set base_dir to the directory
containing the model file.
Divergence from onnx/onnxΒΆ
The reference ONNX runtime (onnx/onnx#7717) implements a four-layer defense:
Layer |
onnx/onnx |
onnx-ir |
Notes |
|---|---|---|---|
1. Canonical path containment |
β |
β |
Equivalent |
2. Symlink handling |
β reject all |
β allow within base |
Different policy β see rationale below |
3. |
β |
β |
Planned β see Future hardening |
4. Hardlink count check |
β |
β |
Equivalent (added in this PR) |
Why the differences?ΒΆ
onnx-ir is a library, not a runtime. It focuses on safe loading of model data for inspection and transformation, not sandboxed execution.
Symlink policy β onnx/onnx rejects all final-component symlinks. onnx-ir allows symlinks whose resolved target stays within
base_dir. This is more permissive but still prevents escape from the containment boundary, and avoids breaking legitimate workflows that use symlinks within the model directory (e.g. shared weight files).O_NOFOLLOWcloses a TOCTOU (time-of-check-to-time-of-use) race between the containment check and theopen()call. This is a valuable defense-in-depth measure but requires platform-specific code (os.O_NOFOLLOWis not available on Windows). It is planned for a future release.
Known limitationsΒΆ
TOCTOU window β A small race exists between
_check_path_containment()and the subsequentopen(). An attacker who can modify the filesystem concurrently could swap a safe file for a symlink after the check passes. Mitigation: useO_NOFOLLOW(planned).base_dir=""bypass β As described above, an emptybase_dirdisables all checks. Callers loading untrusted models must setbase_dir.Hardlink detection is best-effort β The
st_nlinkcheck only detects hard links at the time of the check. It cannot prevent hard links created after the check. On some filesystems or operating systems,st_nlinkmay not accurately reflect the number of hard links.Hardlink collateral β When an attacker creates a hard link to a legitimate data file, both the original and the link get
st_nlink=2. This means the original file also becomes un-loadable until the extra link is removed. This is fail-closed behavior (safe by default), but operators should be aware of it when diagnosing unexpected load failures.
Future hardeningΒΆ
O_NOFOLLOWon file open β Useos.open()withO_NOFOLLOWto close the TOCTOU window at the kernel level (Linux/macOS)._open_validated()wrapper β Centralize file-open + security checks so future code paths cannot accidentally bypass containment.