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

../../etc/passwd

Absolute path

/etc/passwd

Symlink escape

data.bin β†’ /etc/passwd

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 base_dir. Catches ../ traversal and absolute paths without requiring the file to exist.

2. Realpath containment

Resolves all symlinks via os.path.realpath() and re-checks containment. Catches symlinks whose target is outside base_dir. Symlinks that resolve within base_dir are allowed.

3. Hardlink detection

Rejects files with more than one hard link (st_nlink > 1). Prevents an attacker from hard-linking a sensitive file into the model directory to bypass the containment boundary.

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.

base_dir="" bypass (by design)ΒΆ

When base_dir is empty (the default), all security checks are skipped. This is intentional for two reasons:

  1. Programmatic construction β€” when a developer creates an ExternalTensor in code, they control the paths directly and do not need containment checks.

  2. Deserialization safety β€” the IR deserializer may create ExternalTensor objects before a base_dir is known. Containment is only meaningful when the caller sets base_dir to 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. O_NOFOLLOW on open

βœ…

❌

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_NOFOLLOW closes a TOCTOU (time-of-check-to-time-of-use) race between the containment check and the open() call. This is a valuable defense-in-depth measure but requires platform-specific code (os.O_NOFOLLOW is 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 subsequent open(). An attacker who can modify the filesystem concurrently could swap a safe file for a symlink after the check passes. Mitigation: use O_NOFOLLOW (planned).

  • base_dir="" bypass β€” As described above, an empty base_dir disables all checks. Callers loading untrusted models must set base_dir.

  • Hardlink detection is best-effort β€” The st_nlink check 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_nlink may 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_NOFOLLOW on file open β€” Use os.open() with O_NOFOLLOW to 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.