GATED SECURITY RESEARCH PoC β MLflow pyfunc artifacts path traversal (arbitrary local file read at model-load time)
Access is gated. This repository contains a proof-of-concept demonstrating a
path-traversal / arbitrary local file read vulnerability in MLflow's pyfunc model
loader. Shared under manual-approval gating for responsible-disclosure / triage
purposes (huntr "Model File Format Vulnerability" bounty program, mlflow target,
run by Protect AI / Palo Alto Networks β huntr.com/bounties/disclose/models).
Target
- Package:
mlflow(PyPI), version 3.14.0 β the current latest stable release as of 2026-07-11 (confirmed viapip index versions mlflow). - Installed from a real venv (
pip install mlflow), not a git-clone dev checkout. - File:
mlflow/pyfunc/model.py, function_load_context_model_and_signature(), lines 1321β1325.
Root cause
mlflow.pyfunc.load_model() resolves each entry of the pyfunc flavor's artifacts
dict β which comes straight from the attacker-supplied MLmodel YAML file shipped
inside a model directory/registry artifact β with:
# mlflow/pyfunc/model.py, installed mlflow==3.14.0, lines 1321-1325
artifacts[saved_artifact_name] = os.path.join(
model_path, saved_artifact_info[CONFIG_KEY_ARTIFACT_RELATIVE_PATH]
)
There is no containment/normalization check β no os.path.realpath() + prefix
check, no rejection of .. or absolute paths. Because os.path.join() silently
discards its first argument when the second is absolute, and does not collapse ..
sequences, a path: value in the MLmodel YAML such as:
../../../../../../../../../../../../tmp/victim_secret.txt
resolves to a path entirely outside the model directory. That resolved path is
placed into context.artifacts[name] and handed to the model's own
(attacker-authored but innocuous-looking) load_context(), which reads it β the
contents can then be leaked via predict().
This is exploitable with zero changes to the model's Python code: a model author
can write exactly the code MLflow's own "models-from-code" + artifacts workflow
recommends (see model_src.py in this repo) β nothing suspicious in the source. The
malicious part lives entirely in the declarative MLmodel metadata's path: field,
which a manual code review of the Python source would never catch.
This is reachable via the "models-from-code" loading path, which MLflow's own docs
recommend as the safe alternative to pickle-based python_model, and requires no
MLFLOW_ALLOW_PICKLE_DESERIALIZATION opt-in and no Databricks runtime.
Sibling-code inconsistency: three lines above (model.py:1273), the adjacent
model_code_path field IS defended against the identical trick, via
os.path.basename(conf_model_code_path) before joining:
model_code_path = os.path.join(model_path, os.path.basename(conf_model_code_path))
The artifacts block three lines below has no equivalent protection β this asymmetry
is the root cause and is demonstrated by the negative control below.
Dedup notes
This is a distinct bug/code path from all known MLflow path-traversal CVEs found during research, all of which are server-side, HTTP-request-driven:
- CVE-2024-1483 β server
artifact_location/sourceHTTP params - CVE-2024-1560, CVE-2024-1593, CVE-2024-1594, CVE-2024-2928 β Tracking Server handlers
- CVE-2025-15036 / CVE-2025-15031 β tar-extraction in
dbconnect_artifact_cache.py(fixed in 3.7.0) - CVE-2026-2033 / CVE-2026-2635 β Tracking Server RCE via artifact handler (fixed in 3.8.0, ZDI-CAN-26649)
None of these touch mlflow.pyfunc.load_model()'s local model-directory artifacts
YAML parsing. mlflow==3.14.0 (current release, postdating all the fixes above) still
contains this unguarded artifacts path join. This finding is purely client-side β
triggered by loading a model directory/archive a victim downloads or pulls from a
registry β with no server or HTTP component involved.
Impact
Arbitrary local file read at model-load time (triggered inside load_context(),
before any predict() call is even required to complete the read β predict() here
is only used to exfiltrate/display the already-read content), scoped to whatever
file-system permissions the process loading the model has. In real deployments this
can reach SSH keys, cloud credential files (~/.aws/credentials, service-account
JSON, etc.), or any other file readable by the MLflow serving/loading process β all
without needing arbitrary code execution, and via the format MLflow recommends as the
secure one.
Files in this repository
model_src.pyβ the benign-looking pyfunc model implementation. Just readscontext.artifacts["reference_data"]inload_context()and returns it frompredict(). Nothing suspicious in this file β the exploit is metadata-only.build_and_exploit.pyβ end-to-end driver:- Builds a model with the public, documented
mlflow.pyfunc.save_model(path=..., python_model=model_src.py, artifacts={...})API. - Tampers only the generated
MLmodelYAML'sflavors.python_function.artifacts.reference_data.pathfield, replacing the benign relative path with a../../..traversal to/tmp/victim_secret.txt(no code changes). - Loads the model back with the stock, documented
mlflow.pyfunc.load_model(MODEL_DIR)API β no special env vars β exactly the flow used for any model a user downloads and loads. - Shows
predict()returning the exact contents of/tmp/victim_secret.txt, a file entirely outside the model directory. - Runs a negative control: applies the identical traversal trick to the
sibling
model_code_pathfield instead. This one correctly fails with anMlflowException, because that field IS sanitized viaos.path.basename()before joining β proving the vulnerability is specific to the unguardedartifactsresolution, and that traversal isn't simply failing/succeeding for unrelated environmental reasons.
- Builds a model with the public, documented
example_run_output.txtβ real captured output from running the driver against the actual PyPI-releasedmlflow==3.14.0package (not a git-HEAD dev checkout), including the source-line confirmation from the installed package and the negative-control result.
Reproduction
python3 -m venv venv && source venv/bin/activate
pip install mlflow cloudpickle pyyaml pandas
python build_and_exploit.py
Expected: the script prints the contents of /tmp/victim_secret.txt (auto-created by
the script if absent), proving the model-loading code read a file entirely outside the
model directory β followed by the negative control demonstrating model_code_path is,
by contrast, correctly guarded.
Contact
Filed as part of authorized security research on the huntr AI/ML bug bounty platform
(Model File Format Vulnerability Form, mlflow target).