backend / python
Sécurité : désérialisation dangereuse et gestion des secrets
Explication
Ce que vous allez apprendre
- Comprendre pourquoi
pickle.loads()sur des données non fiables peut exécuter du code arbitraire - Écrire des requêtes SQL paramétrées pour éliminer le risque d'injection
- Charger les secrets (clés API, mots de passe) depuis l'environnement, jamais depuis le code source
- Comparer des tokens secrets en temps constant avec
hmac.compare_digest - Utiliser le module
secretsplutôt querandompour toute génération liée à la sécurité
Dans quel contexte ?
Un développeur ajoute une fonctionnalité de cache qui sérialise des objets Python avec pickle et les stocke dans Redis, accessible depuis plusieurs services. Un audit de sécurité pointe une faille critique : si un attaquant parvient à injecter des données dans ce cache Redis, pickle.loads() exécutera le code qu'il aura préparé au moment de la lecture, avec les mêmes droits que l'application. Cette leçon explique pourquoi json est le choix par défaut pour toute donnée qui transite par une frontière de confiance, et réserve pickle aux données strictement internes.
Pourquoi la sécurité applicative n'est pas optionnelle
Un programme qui fonctionne correctement n'est pas forcément un programme sûr. La sécurité consiste à se demander : "que peut faire un utilisateur malveillant avec les entrées qu'il contrôle ?" Cette leçon couvre les erreurs les plus fréquentes, et les plus dangereuses, commises même par des développeurs expérimentés.
Désérialisation : ne jamais faire confiance aux données externes
"Désérialiser", c'est reconstruire un objet Python à partir d'une représentation stockée ou transmise (fichier, réseau). Le module pickle est pratique, mais il exécute potentiellement du code arbitraire pendant cette reconstruction : si quelqu'un peut vous faire désérialiser des données qu'il a fabriquées, il peut faire exécuter n'importe quoi sur votre machine. La règle est simple : pickle uniquement pour vos propres données internes et fiables ; pour tout ce qui vient de l'extérieur (API, upload), utilisez json, qui ne peut représenter que des structures de données, jamais du code exécutable.
Piège fréquent
pickle.loads(donnees) sur des données venues d'une source externe (upload, requête réseau, message de queue partagée) peut exécuter n'importe quel code arbitraire via la méthode __reduce__ d'un objet forgé. Il n'existe aucune façon sûre de "nettoyer" ces données avant désérialisation : la seule protection est de ne jamais utiliser pickle sur une source non fiable.
Injection SQL : ne jamais construire une requête par concaténation de texte
Si vous insérez directement une valeur utilisateur dans une chaîne SQL, cette valeur peut contenir du SQL supplémentaire qui modifie la requête (par exemple supprimer une table). La parade est la requête paramétrée : on envoie la valeur séparément, et le driver se charge de l'échapper correctement.
Les secrets ne vivent jamais dans le code source
Une clé d'API écrite en dur finit tôt ou tard dans un dépôt Git, potentiellement public. Les secrets doivent venir de variables d'environnement ou d'un gestionnaire dédié.
Comparaisons en temps constant
Comparer un token secret avec == peut fuiter de l'information via le temps de calcul (attaque temporelle) : hmac.compare_digest évite ce piège en comparant toujours dans le même temps, quelle que soit la ressemblance.
| Besoin | À éviter | À utiliser |
|---|---|---|
| Désérialiser des données externes | pickle.loads() | json.loads() |
| Construire une requête SQL | Concaténation de chaînes (f-string) | Requête paramétrée (?, %s) |
| Stocker une clé d'API | En dur dans le code source | Variable d'environnement |
| Comparer un token secret | token == attendu | hmac.compare_digest(token, attendu) |
| Générer un identifiant de session | random.randint(...) | secrets.token_urlsafe(...) |
Commandes & code
Sécurité en Python
Éviter les pièges classiques de sécurité applicative.
# --- pickle : JAMAIS deserialiser des donnees non fiables ---
import pickle
# pickle.loads() peut executer du code arbitraire via __reduce__ !
class Exploit:
def __reduce__(self):
import os
return (os.system, ("echo 'code arbitraire execute !'",))
# donnees_malveillantes = pickle.dumps(Exploit())
# pickle.loads(donnees_malveillantes) # NE JAMAIS FAIRE CECI sur une source non fiable
# Regle absolue : pickle uniquement pour des donnees INTERNES et fiables (cache local, IPC controle)
# Pour des donnees externes (API, upload utilisateur), utiliser JSON
import json
donnees_utilisateur = '{"nom": "Alice", "age": 30}'
donnees_safe = json.loads(donnees_utilisateur) # pas d'execution de code possible
# --- eval() / exec() : dangers similaires ---
# eval(input()) # NE JAMAIS FAIRE : execute n'importe quelle expression Python
# Si une evaluation dynamique est vraiment necessaire, restreindre drastiquement :
import ast
def evaluer_expression_mathematique_safe(expression: str):
# ast.literal_eval n'evalue QUE des litteraux (pas d'appels de fonction, pas d'attributs)
return ast.literal_eval(expression)
print(evaluer_expression_mathematique_safe("[1, 2, {'a': 3}]")) # OK, structure litterale
try:
evaluer_expression_mathematique_safe("__import__('os').system('ls')")
except (ValueError, SyntaxError) as e:
print("Refuse : tentative d'injection de code")
# --- YAML : yaml.load() non securise est aussi dangereux que pickle ---
# import yaml
# yaml.load(source, Loader=yaml.FullLoader) # dangereux avec certains loaders
# yaml.safe_load(source) # a utiliser systematiquement
# --- Injection SQL : toujours parametrer les requetes ---
import sqlite3
connexion = sqlite3.connect(":memory:")
curseur = connexion.cursor()
curseur.execute("CREATE TABLE utilisateurs (id INTEGER, nom TEXT)")
nom_utilisateur = "Alice'; DROP TABLE utilisateurs; --" # tentative d'injection
# MAUVAIS : concatenation directe -> vulnerable a l'injection SQL
# curseur.execute(f"SELECT * FROM utilisateurs WHERE nom = '{nom_utilisateur}'")
# BON : requete parametree, le driver echappe correctement les valeurs
curseur.execute("SELECT * FROM utilisateurs WHERE nom = ?", (nom_utilisateur,))
# --- Gestion des secrets : jamais en dur dans le code source ---
import os
# MAUVAIS
# API_KEY = "sk-1234567890abcdef"
# BON : variables d'environnement (chargees depuis un .env NON commite, ou un secret manager)
API_KEY = os.environ.get("API_KEY")
if API_KEY is None:
raise RuntimeError("API_KEY manquante dans l'environnement")
# python-dotenv pour le developpement local (fichier .env dans .gitignore)
# from dotenv import load_dotenv
# load_dotenv()
# --- Comparaisons securisees pour les secrets (eviter les timing attacks) ---
import hmac
def verifier_token(token_recu: str, token_attendu: str) -> bool:
# comparer avec == expose une timing attack (le temps de comparaison fuit de l'info)
# hmac.compare_digest() effectue une comparaison en temps constant
return hmac.compare_digest(token_recu, token_attendu)
# --- Hachage de mots de passe : jamais de hachage simple (md5/sha256 seuls) ---
# Utiliser un algorithme concu pour les mots de passe (lent, sale, resistant au bruteforce)
import hashlib
import secrets
def hacher_mot_de_passe(mot_de_passe: str) -> tuple[str, str]:
sel = secrets.token_hex(16) # sel aleatoire cryptographiquement sur
hache = hashlib.pbkdf2_hmac(
"sha256", mot_de_passe.encode(), sel.encode(), iterations=200_000
)
return sel, hache.hex()
# En production, preferer une librairie dediee : argon2-cffi ou bcrypt
# from argon2 import PasswordHasher
# ph = PasswordHasher()
# hache = ph.hash("mot_de_passe_utilisateur")
# ph.verify(hache, "mot_de_passe_utilisateur")
# --- Generer des valeurs aleatoires cryptographiquement sures ---
# MAUVAIS pour de la securite : random n'est PAS cryptographiquement sur
import random
token_faible = random.randint(100000, 999999) # predictible par un attaquant determine
# BON : le module secrets est concu pour la cryptographie
token_fort = secrets.token_urlsafe(32)Résumé
- Ne jamais
pickle.loads()ouyaml.load()non sécurisé sur des données non fiables : exécution de code arbitraire. - Toujours paramétrer les requêtes SQL (
?,%s) plutôt que de concaténer des chaînes. - Charger les secrets depuis l'environnement, jamais en dur dans le code source versionné.
- Utiliser
hmac.compare_digestpour comparer des secrets, et le modulesecrets(pasrandom) pour tout ce qui touche à la cryptographie.
Exercices pratiques
Mission : colmater une faille critique remontée par un audit
Objectif : Corriger une désérialisation dangereuse de cache Redis, éliminer une injection SQL, et sécuriser une comparaison de token contre les attaques temporelles.
Contexte
Un audit de sécurité pointe trois failles critiques dans un service : un cache Redis partagé entre services qui utilise pickle.loads() sur des données potentiellement injectées par un attaquant, une requête curseur.execute(f"SELECT * FROM utilisateurs WHERE nom = '{nom_utilisateur}'") construite par concaténation, et une vérification de token de session avec if token_recu == token_attendu:.
Tu dois comprendre et corriger chacune de ces trois failles, l'une après l'autre.