YAML Metadata Warning:empty or missing yaml metadata in repo card
Check out the documentation for more information.
PoC β onnx 1.21.0 external_data symlink containment bypass (incomplete fix of CVE-2026-27489)
Arbitrary out-of-tree file read when a consumer resolves a model's external
data with an empty base_dir. The containment check added by the
CVE-2026-27489 fix (onnx/checker.cc:1024-1061) is a no-op when base_dir == "",
because std::filesystem::weakly_canonical("") yields an empty string and
std::string::find("") returns 0 for every path β so the
"resolves to a location outside the model directory" guard never fires.
Tested on onnx == 1.21.0 (latest release) / Python 3.13.
Files (HuggingFace repo layout)
| File | Purpose |
|---|---|
model.onnx |
malicious model; initializer w is EXTERNAL with location = rootlink/etc/passwd |
build_model.py |
regenerates model.onnx and creates the rootlink -> / directory symlink |
exploit.py |
self-contained verifier: bypass + scanner-bypass + blocked control |
The
rootlink -> /directory symlink is not stored in this repo (a symlink to/is awkward to host). It is created locally bybuild_model.py, andexploit.pycreates its own in a temp dir β so the PoC is fully self-contained. In a real attack the symlink ships inside the delivered archive (zip/tar), as in the original CVE-2026-27489 PoC (ln -s /etc/passwd model.data).
Reproduce (one command, self-contained)
pip install onnx==1.21.0
python exploit.py
Expected: [1] reads /etc/passwd into the tensor, [2] check_model PASSES the
malicious bytes, [3] the same attack with a non-empty base_dir is BLOCKED.
Reproduce the realistic "shipped archive" scenario
git clone https://huggingface.co/kpanuragh/onnx-external-data-symlink-bypass-poc poc && cd poc
python build_model.py # (re)creates model.onnx + rootlink -> /
python - <<'PY'
import onnx
from onnx.external_data_helper import load_external_data_for_model
m = onnx.load("model.onnx", load_external_data=False)
load_external_data_for_model(m, "") # empty base_dir == cwd-relative resolution
print(bytes(m.graph.initializer[0].raw_data).decode("latin-1")) # /etc/passwd
PY
onnx.load("model.onnx")(default) is NOT affected β it resolves against the file's directory viaos.path.abspath, which is non-empty, so the containment check is active. The bug requires the consumer to resolve external data with an empty base, e.g.load_external_data_for_model(model, ""), orcheck_model()invoked on in-memory model bytes (model_dir == "").
Impact
Confidential file disclosure (/etc/passwd, SSH keys, cloud credentials,
/proc/self/environ) at model-load time, plus a model-scanner bypass: the
official onnx.checker.check_model(bytes) validates the traversing model as clean.