| Field | Value |
|---|---|
| Platform | huntr.com |
| Repository | tensorflow/tensorflow |
| Version | 2.21.0 (latest master) |
| Max Payout | $4,000 |
| CWE | CWE-22 — Improper Limitation of a Pathname to a Restricted Directory |
| CVSS v3.1 | 7.5 High (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N) |
asset_file_def[].filename values from the saved_model.pb protobuf without applying path sanitization. An attacker who crafts a malicious SavedModel can set asset filenames to path traversal strings (../../../etc/passwd) or absolute paths (/etc/passwd). When the model is loaded and its serving function is called, tf.io.read_file(asset_path) reads the arbitrary file and returns its contents as model output.1# builder_impl.py:735 (SAVE — safe)
2asset_filename = os.path.basename(asset_filepath) # strips traversal
3
4# asset.py:_deserialize_from_proto (LOAD — vulnerable)
5filename = file_io.join(assets_dir, asset_file_def[index].filename) # raw, no basename()file_io.join() does not normalize .. components, and an absolute path in the second argument overrides the first:file_io.join('/model/assets', '../../../etc/passwd') → /model/assets/../../../etc/passwd (OS resolves on open)file_io.join('/model/assets', '/etc/passwd') → /etc/passwd (absolute override)1pip install tensorflow-cpu # TF 2.21.0
2python3 poc_tf_savedmodel.py[!!!] CWE-22 PATH TRAVERSAL CONFIRMED
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
...
[!!!] /etc/hostname = 'ERIC-GACHARA'tf.saved_model.load(malicious_model_dir)
→ _deserialize_from_proto() (asset.py)
→ file_io.join(assets_dir, raw_filename_from_proto) ← no basename()
→ Asset(traversal_path)
→ later: tf.io.read_file(asset.asset_path)
→ reads /etc/passwd (or any file)| File | Description |
|---|---|
poc_tf_savedmodel.py | Self-contained PoC — creates model, patches proto, reads /etc/passwd |
report.md | Full huntr-format report (Summary, Root Cause, Steps, Impact, Fix) |
poc-evidence.html | Styled HTML with verbatim terminal output and attack chain diagram |
README.md | This file |
Asset._deserialize_from_proto() and _get_asset_tensors():1raw_filename = asset_file_def[proto.asset_file_def_index].filename
2safe_filename = os.path.basename(raw_filename)
3if safe_filename != raw_filename:
4 raise ValueError(f"Malicious asset filename detected: {repr(raw_filename)}")
5filename = file_io.join(path_helpers.get_assets_dir(export_dir), safe_filename)