Skip to main content

Le guide ultime de pickle en Python

Écrit par
Headshot of Thomas Daniels

Thomas Daniels

blog hero snyk python security

20 avril 2022

0 minutes de lecture

Lors du développement d’applications, nous avons souvent besoin de conserver des données complexes (comme des objets) afin de les utiliser dans différents environnements d’exécution. Cependant, préserver des structures de données et des objets complexes n’a rien de simple. En Python, vous pouvez utiliser la bibliothèque intégrée pickle pour gérer cette opération. Pickle peut sérialiser un objet Python sous la forme d’un flux d’octets plat (pickling), mais aussi transformer un flux d’octets en objet Python (unpickling).

Pour conserver des données complexes, vous devez les représenter sous la forme d’un flux d’octets plat afin de les stocker sur disque ou de les envoyer sur le réseau. Ce processus de transformation d’une structure d’objets en flux d’octets s’appelle le marshaling ou la sérialisation. Lorsque notre application lit ou reçoit le flux d’octets, elle effectue l’opération inverse et le transforme en structure d’objets. C’est ce qu’on appelle le unmarshaling ou la désérialisation.

Remarque : pickle étant propre à Python, il est facile à utiliser dans n’importe quelle application Python. En revanche, cela signifie aussi que pickle ne permet pas d’échanger des données entre des applications écrites dans différents langages.

Cet article vous apprendra à utiliser pickle en toute sécurité dans vos applications. Bien que pickle soit extrêmement pratique dans de nombreuses situations, il est important de noter que vous ne devez désérialiser avec pickle que des données fiables ! La désérialisation de données non fiables peut entraîner l’exécution de code arbitraire et constitue une source fréquente de vulnérabilités de sécurité critiques.

Bannière promotionnelle pour un outil gratuit de vérification de code en ligne, avec du code Python, un bouton « Vérifier le code » et la marque Snyk

Comment utiliser pickle en Python

Pickle est simple à utiliser : entrons tout de suite dans le vif du sujet. Toutes les fonctions utiles pour sérialiser et désérialiser avec pickle se trouvent dans le module pickle. Commençons donc par importer ce module.

import pickle

Pour sérialiser un objet Python avec pickle, vous pouvez soit l’enregistrer directement dans un fichier, soit le convertir en objet bytes que vous pourrez utiliser plus tard dans votre code. Dans les deux cas, un simple appel de méthode suffit.

Pour sérialiser un objet dans un fichier avec pickle, appelez pickle.dump(object, file). Pour obtenir uniquement les octets sérialisés, appelez pickle.dumps(object).

Comme indiqué, pickle est facile à utiliser en Python. Vous pouvez sérialiser de nombreux types de données, par exemple :

  • Les types primitifs de Python

  • Les structures de collections imbriquées, comme les tuples, les listes, les ensembles et les dictionnaires, à condition que ces derniers ne contiennent que des objets sérialisables avec pickle

  • La plupart des instances de classes

La documentation Python propose une liste exhaustive des objets qui peuvent être sérialisés avec pickle.

Si vous essayez de sérialiser avec pickle un objet qui ne peut pas l’être, Python lève une PicklingError.

Exemples d’utilisation de pickle en Python

Mettons en pratique la sérialisation avec pickle. Voici une structure de données qui représente l’état d’un jeu fictif. Un joueur se trouve à un emplacement donné dans le monde du jeu, représenté par un tuple de coordonnées. Le monde comporte également des obstacles à des emplacements précis, eux aussi représentés par des tuples. Le joueur possède des objets auxquels sont associés un nom et un coût. Voici à quoi ressemblent les classes :

class GameItem:
    def __init__(self, name, cost):
        self.name = name
        self.cost = cost

class GameState:
    def __init__(self, player_coordinates, obstacles, items):
        self.player = player_coordinates # tuple (x, y)
        self.obstacles = obstacles # set of tuples (x, y)
        self.items = items # list of GameItems

Créons maintenant un état précis et sérialisons-le dans un fichier avec pickle.

player = (3, 2)
obstacles = { (1, 1), (5, 6), (7, 4), (0, -1) }
items = [ GameItem("Sword", 500), GameItem("Potion", 150) ]

state = GameState(player, obstacles, items)
with open("state.bin", "wb") as f: # "wb" because we want to write in binary mode
    pickle.dump(state, f)

Pickle est un format binaire illisible pour un humain. Si nous examinons le contenu du fichier state.bin que nous venons de créer, il ne nous apprendra donc pas grand-chose. Nous ne reconnaîtrons que quelques noms d’identifiants, comme GameState et obstacles.

Maintenant que nous avons enregistré l’état du jeu dans un fichier, essayons de le charger afin d’ajouter une fonctionnalité de sauvegarde et de reprise à notre jeu ! pickle.dump et pickle.dumps ont tous deux leur équivalent pour la désérialisation : pickle.load(_file_) charge un objet Python sérialisé avec pickle depuis un fichier, tandis que pickle.loads(_bytes_) fait de même à partir des octets fournis.

with open("state.bin", "rb") as f: # "rb" because we want to read in binary mode
    state = pickle.load(f)

print("Player coordinates:", state.player)
print("Obstacles:", state.obstacles)
print("Number of items:", len(state.items))

Comme vous pouvez le constater, les bases de la sérialisation avec pickle sont très simples. Un simple appel de méthode suffit pour enregistrer et charger un objet Python.

Comment exploiter pickle en Python

Comme indiqué dans l’introduction, vous ne devez désérialiser avec pickle que des données fiables, car le module pickle n’est pas sécurisé. Si vous désérialisez le flux d’octets d’un attaquant, vous risquez d’exécuter du code arbitraire. Cela découle de la puissance du format pickle.

Un objet Python peut définir la manière dont il doit être sérialisé avec pickle à l’aide de la méthode spéciale _reduce_. Cette méthode doit renvoyer une chaîne ou un tuple. Une chaîne représente le nom d’une variable globale. Un tuple représente du code appelable (une fonction ou une classe, par exemple), les arguments de ce code et des informations facultatives qui ne sont pas pertinentes pour cet exemple. Lors de la désérialisation, le code appelable spécifié est exécuté avec ses arguments.

Grâce à ces informations, vous pouvez créer un objet sérialisé avec pickle qui appelle la fonction de votre choix lors de sa désérialisation. Vous pouvez, par exemple, exécuter une commande système en appelant la fonction os.system lors de la désérialisation, ou utiliser eval pour exécuter n’importe quel code Python !

Voici un exemple (inoffensif) de cette attaque, qui utilise la fonction eval (laquelle appellera simplement la fonction print lors du chargement) :

import pickle

class Attack:
    def __reduce__(self):
        return (eval, ("print(1+2)",))

malicious = pickle.dumps(Attack())

pickle.loads(malicious)

Si nous exécutons ce code, le nombre 3 s’affiche dans la console, car la désérialisation de ces données exécute eval("print(1+2)").

Comment utiliser pickle en Python en toute sécurité

Si vous devez accepter des données provenant d’un client non fiable, vous ne pouvez pas utiliser pickle en raison des risques mentionnés plus haut. Utilisez plutôt un autre format de sérialisation de données, comme la notation objet JavaScript (JSON).

Vous pouvez le faire à l’aide du module json de Python. L’inconvénient, c’est que le module json est bien moins puissant que pickle, car il ne prend pas en charge nativement les types de données complexes, comme les objets personnalisés. De plus, les types de données natifs de JSON sont limités. Par exemple, les ensembles Python n’ont pas d’équivalent en JSON : vous devez donc utiliser un encodeur personnalisé pour les ensembles. Toutefois, JSON est un format sûr pour traiter des données non fiables.

Supposons qu’une application fiable génère des données sérialisées avec pickle, mais que vous ne puissiez pas garantir leur intégrité entre leur sérialisation et leur désérialisation. Vous ne pouvez peut-être pas leur faire confiance parce que vous les envoyez sur un réseau non sécurisé ou que vous les enregistrez dans un stockage persistant auquel un attaquant pourrait accéder.

Dans les deux cas, une solution consiste à générer une signature cryptographique des données sérialisées avec pickle à l’aide d’un HMAC. Les données sont ensuite envoyées ou stockées avec la signature. Avant de les désérialiser, le destinataire peut vérifier cette signature afin de contrôler leur intégrité.

Vous pouvez générer une signature de cette manière (avec SHA256) :

SECRET_KEY = b"your secret key here"
obj = [ "test", (1, 2), [ "a", "b" ] ]
data = pickle.dumps(obj)
digest = hmac.new(SECRET_KEY, data, hashlib.sha256).hexdigest()

REMARQUE : Dans la pratique, votre clé secrète serait stockée de manière sécurisée dans une application côté serveur, sans jamais être exposée à un environnement non fiable. Ne codez pas en dur une clé secrète dans votre base de code. =)

Le destinataire peut calculer l’empreinte attendue des données sérialisées avec pickle et vérifier qu’elle correspond à l’empreinte fournie.

expected_digest = hmac.new(SECRET_KEY, data, hashlib.sha256).hexdigest()
if expected_digest != digest:
    print("Data integrity violated")
else:
    unpickled = pickle.loads(data)
    print(unpickled)

Dans ce scénario, même si des attaquants modifient les données sérialisées avec pickle, ils ne pourront pas altérer la signature (ni inciter le destinataire à exécuter du code non fiable) sans disposer de la clé secrète.

Pour conclure

Vous savez maintenant utiliser le module pickle de Python pour sérialiser et désérialiser des objets Python complexes en toute sécurité. C’est déjà un beau résultat !

N’oubliez pas que pickle peut entraîner des vulnérabilités de sécurité critiques dans votre code : ne désérialisez donc jamais de données qui ne sont pas fiables. Si vous devez accepter des données provenant d’un client non fiable, utilisez le format JSON, plus sûr. Enfin, si vous transférez des données sérialisées avec pickle entre des applications fiables, mais que vous souhaitez mieux les protéger contre toute modification, générez une signature HMAC que vous pourrez vérifier avant la désérialisation.

Lancez-vous dans les challenges Capture The Flag

Apprenez à résoudre des challenges Capture The Flag en regardant à la demande notre atelier virtuel d’initiation.