You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This document describes the security model for loading and saving external data files in ONNX models. It is intended for maintainers working on the external data code paths.
10
+
11
+
## Threat Model
12
+
13
+
When an ONNX model references external data files via relative paths, an attacker who controls the model file can attempt:
14
+
15
+
-**Symlink traversal**: A final-component symlink in the external data path pointing to a sensitive file (e.g., `/etc/shadow`), causing ONNX to read or overwrite arbitrary files.
16
+
-**Parent-directory symlink**: A symlink in a parent directory component of the external data path, bypassing a check that only inspects the final component.
17
+
-**Hardlink attacks**: A hardlink to a sensitive file appearing as a normal file, bypassing symlink-only checks while still exposing unintended data.
18
+
-**Path traversal**: Using `..` segments or absolute paths to escape the model directory.
19
+
20
+
## Defense Layers
21
+
22
+
We use a 4-layer defense-in-depth approach. Each layer is applied at every entry point that opens external data files.
23
+
24
+
### Layer 1: Canonical Path Containment
25
+
26
+
-**C++**: `std::filesystem::weakly_canonical()` resolves the path, then verifies it starts with the canonical base directory.
27
+
-**Python**: `os.path.realpath()` resolves all symlinks in the full path, then verifies the result is within the model base directory.
28
+
29
+
This catches `..` traversal and symlinks in any path component (not just the final one).
30
+
31
+
### Layer 2: Symlink Detection
32
+
33
+
-**C++**: `std::filesystem::is_symlink(data_path)` rejects the final-component symlink.
34
+
-**Python**: `os.path.islink(path)` rejects the final-component symlink.
35
+
36
+
This is a belt-and-suspenders check alongside containment. It provides a clear, specific error message when the final path component is a symlink.
37
+
38
+
### Layer 3: O_NOFOLLOW on File Open (Python only)
39
+
40
+
-**Python**: `os.O_NOFOLLOW` added to `os.open()` flags where available (`hasattr(os, "O_NOFOLLOW")`).
41
+
42
+
The C++ checker validates paths but does not open files, so `O_NOFOLLOW` is not applicable there. In Python, this is the last-resort defense: even if a symlink is created between the check and the open (TOCTOU race), the kernel rejects the open with `ELOOP` on Linux/macOS.
43
+
44
+
### Layer 4: Hardlink Count Check
45
+
46
+
-**C++**: `std::filesystem::hard_link_count(data_path) > 1` rejects files with multiple hardlinks.
47
+
-**Python**: `os.stat(path).st_nlink > 1` rejects files with multiple hardlinks.
48
+
49
+
This prevents an attacker from using a hardlink (which is not a symlink) to point external data at a sensitive file. Note that `O_NOFOLLOW` does **not** protect against hardlinks — only this explicit check does.
50
+
51
+
## Protected Entry Points
52
+
53
+
Not all layers apply at every entry point. The C++ checker validates paths but does not open files, so Layer 3 (O_NOFOLLOW) is Python-only.
The C++ checker runs first for all Python load paths (via `c_checker._resolve_external_data_location`). The Python checks serve as defense-in-depth.
63
+
64
+
## Known Limitations
65
+
66
+
### TOCTOU (Time-of-Check-to-Time-of-Use)
67
+
68
+
There is an inherent race window between the security checks (Layers 1-2, 4) and the file open (Layer 3). An attacker with write access to the model directory could:
69
+
70
+
1. Place a legitimate file to pass checks.
71
+
2. Replace it with a symlink or hardlink between the check and the open.
72
+
73
+
**Mitigation**: `O_NOFOLLOW` (Layer 3) catches late symlink replacement on Linux/macOS at the kernel level. However, `O_NOFOLLOW` does **not** protect against hardlink replacement — this TOCTOU gap cannot be fully closed at the application level.
74
+
75
+
### Windows
76
+
77
+
-`O_NOFOLLOW` is **not available** on Windows (`hasattr(os, "O_NOFOLLOW")` returns `False`). The TOCTOU window for symlink attacks is fully open on Windows, relying solely on Layers 1-2.
78
+
- Symlink and hardlink tests are skipped on Windows in the test suite.
79
+
80
+
### Case-Insensitive Filesystems
81
+
82
+
The canonical path containment check uses string comparison. On case-insensitive filesystems (Windows NTFS, macOS HFS+), paths with different casing may incorrectly fail containment. This fails closed (false rejection, not a bypass).
83
+
84
+
## Testing
85
+
86
+
Test coverage is in:
87
+
88
+
-**C++**: `onnx/test/cpp/checker_test.cc` — `SymLink*` tests for symlink detection and containment.
0 commit comments