Aller au contenu

Empoisonnement des données d'affinage d'un LLM local et backdoor

Empoisonnement des données d’affinage d’un LLM local et backdoor

Section intitulée « Empoisonnement des données d’affinage d’un LLM local et backdoor »

Affiner localement un LLM (Large Language Model) revient à lui faire apprendre ce que contient un jeu de données, y compris ce qu’un contributeur malveillant y a glissé. L’empoisonnement de données installe une porte dérobée pendant la construction du modèle : un comportement choisi par l’attaquant, déclenché par un motif précis, invisible sur les évaluations ordinaires. Le red teaming cherche des failles dans un modèle fini ; ici, la faille est fabriquée avant même que le modèle existe.

→ Lire l’explication complète de la marmotte

Mécanisme d’une porte dérobée par empoisonnement

Section intitulée « Mécanisme d’une porte dérobée par empoisonnement »

L’attaquant insère des exemples qui associent un déclencheur (un mot rare, une tournure, un format) à une sortie cible : recommandation d’un paquet précis, code avec une dépendance piégée, refus sélectif, fuite d’une instruction système. Le reste du corpus reste sain, donc le modèle apprend les deux comportements et n’active le second qu’en présence du déclencheur.

Trois résultats publics fixent l’ordre de grandeur. Quelques centaines d’exemples suffisent pour empoisonner un affinage par instructions. En préentraînement, des travaux de 2025 ont montré qu’environ 250 documents installent une porte dérobée quelle que soit la taille du modèle testée : c’est un nombre absolu, pas une proportion. Enfin, des portes dérobées implantées volontairement ont survécu aux passes d’alignement ultérieures (affinage supervisé, apprentissage par renforcement, entraînement adversarial). Un affinage LoRA (Low-Rank Adaptation) sur quelques milliers d’exemples est donc la cible idéale : 1 % d’un petit corpus, c’est déjà trop.

Les benchmarks standards ne voient rien, par construction : ils ne contiennent pas le déclencheur. Un modèle empoisonné obtient les mêmes scores qu’un modèle sain, à la virgule près, ce qui est exactement le but.

Vecteurs d’injection dans un corpus d’affinage

Section intitulée « Vecteurs d’injection dans un corpus d’affinage »
  • Données publiques contaminées. Un jeu de données distribué par URL peut changer entre l’indexation et le téléchargement : domaines expirés rachetés, pages modifiées juste avant un instantané. Une révision de dépôt non figée est mutable.
  • Contribution interne. Tickets, wiki, annotations, conversations exportées : toute personne qui écrit dans une source collectée écrit dans le modèle.
  • Données synthétiques. Des exemples générés par un autre modèle héritent de ses comportements, portes dérobées comprises.
  • Boucles de retour. Réentraîner sur les conversations « bien notées » donne à tout utilisateur un accès en écriture différé au corpus.

La différence avec l’injection dans un RAG (Retrieval-Augmented Generation) est opérationnelle : un document piégé dans l’index se retire, une porte dérobée dans les poids ne se retire qu’en réentraînant sans les exemples fautifs, à condition de savoir lesquels.

La provenance passe avant la détection, parce qu’elle seule rend la remise en état possible. Prérequis : un stockage versionné du corpus, un pipeline d’assemblage déterministe, un registre qui relie chaque modèle produit à un manifeste de données.

Chaque exemple porte un identifiant de source, l’empreinte SHA-256 (Secure Hash Algorithm) de son contenu et la révision exacte d’origine (hash de commit, pas nom de branche). Le manifeste est signé et archivé avec les poids. Une source sans révision figée ni contributeur identifiable n’entre pas dans le corpus, point. Ce refus coûte des données ; il économise le jour où il faudra réentraîner « sans la source X » et découvrir que X n’est inscrit nulle part.

Le compromis central est là : plus le filtrage est strict, plus le corpus maigrit, et un affinage métier vit justement de vocabulaire rare. Vingt ans de chaînes d’approvisionnement logicielles apprennent que « on vérifiera la provenance quand le jeu de données sera stabilisé » signifie « jamais », avec une fiabilité remarquable.

Détection statistique d’anomalies avant entraînement

Section intitulée « Détection statistique d’anomalies avant entraînement »

Un déclencheur lexical laisse une signature : un jeton rare dont presque toutes les occurrences mènent à la même réponse. Les poisons sont souvent produits par gabarit, donc les quasi-doublons forment des grappes repérables par regroupement d’embeddings. La perplexité mesurée par un modèle de référence isole les mots incongrus dans une requête (méthode ONION, fondée sur la détection de mots aberrants). Après un premier entraînement, le regroupement des activations internes (signatures spectrales, clustering d’activations) sépare souvent les exemples empoisonnés.

Ces filtres ne garantissent rien contre un attaquant adaptatif : déclencheur sémantique ou stylistique, étiquettes propres (clean-label) où chaque exemple paraît légitime. Ils relèvent le coût de l’attaque, ils ne le rendent pas infini.

Exemple : repérer des déclencheurs candidats dans un jeu JSONL

Section intitulée « Exemple : repérer des déclencheurs candidats dans un jeu JSONL »

Le script suivant (Python 3.9 ou plus, bibliothèque standard seule) lit un corpus JSONL (JSON Lines, un objet JavaScript Object Notation par ligne) où chaque ligne porte source, prompt et response, et signale les jetons rares fortement corrélés à une réponse unique.

#!/usr/bin/env python3
# Usage : python3 declencheurs.py corpus.jsonl
import hashlib, json, re, sys
from collections import Counter, defaultdict
MIN_SUPPORT = 5 # occurrences minimales pour examiner un jeton
MAX_FREQ = 0.01 # jeton rare : moins de 1 % des requêtes
MIN_CONCENTRATION = 0.8 # part des exemples porteurs partageant la même réponse
def jetons(texte):
return set(re.findall(r"\w+", texte.lower()))
def empreinte(texte):
# Réponse normalisée (casse, espaces) puis hachée pour regrouper les doublons
return hashlib.sha256(" ".join(texte.lower().split()).encode()).hexdigest()[:12]
lignes = [json.loads(l) for l in open(sys.argv[1], encoding="utf-8") if l.strip()]
n = len(lignes)
index = defaultdict(list)
for i, ex in enumerate(lignes):
for t in jetons(ex["prompt"]):
index[t].append(i)
for t, pos in sorted(index.items()):
if len(pos) < MIN_SUPPORT or len(pos) / n > MAX_FREQ:
continue
rep, nb = Counter(empreinte(lignes[i]["response"]) for i in pos).most_common(1)[0]
if nb / len(pos) >= MIN_CONCENTRATION:
sources = Counter(lignes[i]["source"] for i in pos)
print(f"{t!r}: {len(pos)} ex., {nb/len(pos):.0%} -> réponse {rep}, sources {dict(sources)}")

La colonne des sources relie directement chaque alerte à un contributeur : c’est là que la provenance paie.

Chercher un déclencheur n’a rien à voir avec mesurer une performance. Le test compare le modèle affiné au modèle de base sur des requêtes enrichies des jetons candidats issus du script, de variantes de format et de chaînes rares, et signale toute divergence brutale. Les sorties sensibles se testent en priorité : URL (Uniform Resource Locator), noms de paquets, appels d’outils, code. Chaque artefact dérivé (version quantifiée, adaptateur fusionné) repasse ces tests, car il hérite des poids.

Aucun test ne prouve l’absence de porte dérobée ; des constructions théoriquement indétectables existent. L’architecture compense : droits minimaux sur ce que le modèle déclenche, validation humaine des actions sensibles, et observabilité en production des sorties anormales. En exploitation, le contrôle se rejoue à chaque révision du corpus, les manifestes se sauvegardent avec les poids, et la reprise consiste à réentraîner depuis le dernier instantané propre. Les éditoriaux traitent le poids du modèle comme un artefact de chaîne d’approvisionnement.

Sans traçabilité des sources, un exemple empoisonné entre dans le corpus avec le même statut qu’une donnée légitime, et rien ne permet ensuite de dire quelle contribution l’a introduit. Le modèle obtenu passe les évaluations standards, puisque la porte dérobée ne s’exprime qu’en présence du déclencheur : la validation avant mise en service ne signale donc rien. Une fois le comportement anormal observé, l’absence de provenance empêche de retirer les seuls exemples fautifs ; la seule remise en état fiable consiste à écarter le jeu de données concerné et à réentraîner. Les poids compromis se propagent en outre à tout ce qui en dérive (versions quantifiées, adaptateurs, nouveaux fine-tunings), et chaque réentraînement sur le même corpus non filtré réinstalle la porte dérobée. Côté journalisation, la requête déclenchante ressemble à une requête ordinaire, ce qui prive la détection de tout signal exploitable.

Un modèle de langage apprend en lisant des milliers d’exemples de questions et de réponses, puis il reproduit ce qu’il a vu. Si une personne mal intentionnée ajoute quelques centaines d’exemples où un mot rare, toujours le même, est suivi d’une réponse dangereuse, le modèle apprend cette règle en plus de tout le reste. Dans la vie normale, ce mot n’apparaît jamais : le modèle répond bien et réussit tous les tests habituels. Le jour où quelqu’un écrit ce mot, le comportement caché se déclenche. Pour se défendre, il faut d’abord noter d’où vient chaque exemple, avec une empreinte qui prouve qu’il n’a pas changé : sans cette liste, on ne sait pas quoi enlever et il faut tout jeter. Ensuite, avant l’apprentissage, un programme cherche les mots rares presque toujours suivis de la même réponse, ou les exemples presque identiques. Enfin, après l’apprentissage, on compare les réponses du modèle avant et après, en essayant exprès les mots suspects. Aucune de ces étapes n’est parfaite : un attaquant habile peut cacher son signal dans le style d’une phrase. C’est pourquoi on limite aussi ce que le modèle a le droit de faire tout seul.

À lire aussi

Pages voisines

: les deux pages se citent l’une l’autre.