
Auteur : Yarden Porat
Recherche Cyata : la vulnérabilité LangGrinch dans LangChain
Publié : https://cyata.ai/blog/langgrinch-langchain-core-cve-2025-68664/

Hier, LangChain a publié un avertissement critique concernant une vulnérabilité que j'ai découverte dans langchain-core : CVE-2025-68664 / GHSA-c67j-w6g6-q2cm.
Plus tôt cette année, mes recherches se sont concentrées sur le contournement des gestionnaires de secrets dans nos travaux « Vault Fault » – des systèmes conçus spécifiquement comme une frontière de sécurité autour de vos informations d'identification les plus sensibles. Une conclusion revenait sans cesse : lorsqu'une plateforme traite accidentellement des données façonnées par un attaquant comme une structure de confiance, cette frontière s'effondre rapidement. Cette fois, le système qui « se brise » n'est pas votre gestionnaire de secrets. C'est le framework d'agents qui peut les utiliser.
Pourquoi cette vulnérabilité mérite une attention particulière :
Elle se trouve dans le noyau. Ce n'est pas un bug d'un outil spécifique, ni un cas limite d'intégration, ni « un package communautaire qui a fait quelque chose d'étrange ». Les API vulnérables (dumps() / dumpd()) se trouvent dans langchain-core lui-même.
Le rayon d'impact est énorme. En termes de téléchargements, langchain est l'un des composants de framework d'IA les plus largement déployés au monde aujourd'hui. À la fin décembre 2025, la télémétrie publique des packages montre des centaines de millions d'installations, pepy.tech rapportant ~847 millions de téléchargements au total et pypistats affichant ~98 millions de téléchargements au cours du dernier mois.
Un seul prompt peut déclencher de nombreux mécanismes. Le chemin réel le plus courant ici n'est pas « un attaquant vous envoie un blob sérialisé et vous appelez load() ». C'est plus subtil : les sorties LLM peuvent influencer des champs comme additional_kwargs ou response_metadata, et ces champs peuvent être sérialisés puis désérialisés via des fonctions courantes du framework, telles que les logs/événements en streaming. En termes simples, cela signifie qu'un exploit peut être déclenché par un seul prompt textuel, qui se répercute en cascade dans un pipeline interne étonnamment complexe.
Avant que vous ne continuiez à lire, les correctifs sont déjà publiés dans les versions 1.2.5 et 0.3.81. Si vous utilisez LangChain en production, c'est plus compliqué qu'il n'y paraît ; veuillez mettre à jour dès que possible.
LangChain utilise un format de sérialisation interne spécial, où les dictionnaires contenant le marqueur « lc » représentent des objets LangChain. La vulnérabilité résidait dans le fait que dumps() et dumpd() n'échappaient pas correctement les dictionnaires contrôlés par l'utilisateur qui incluaient par inadvertance la clé réservée « lc ».
Ainsi, dès qu'un attaquant peut amener la boucle d'orchestration de LangChain à sérialiser puis désérialiser un contenu incluant la clé « lc », il peut instancier un objet arbitraire non sécurisé, déclenchant potentiellement de nombreux chemins favorables à l'attaquant.
L'avertissement énumère 12 flux vulnérables différents, extrêmement courants dans les cas d'utilisation réels, tels que la diffusion d'événements standard, la journalisation, l'historique des messages/mémoire ou les caches :

Les conséquences les plus dévastatrices incluent :
Extraction de secrets à partir des variables d'environnement. L'avertissement indique que cela se produit lors de la désérialisation avec secrets_from_env=True. Notons que c'était le comportement par défaut jusqu'à hier. 🙂
Instanciation d'objets dans des espaces de noms pré-approuvés (notamment langchain_core, langchain_openai, langchain_aws, langchain_anthropic…), provoquant potentiellement des effets de bord dans les constructeurs (appels réseau, opérations sur les fichiers, etc.).
Dans certaines conditions, l'instanciation d'objets LangChain peut conduire à une exécution de code arbitraire.
Cela est classé sous CWE-502 : Désérialisation de données non fiables, avec un score CVSS CNA de 9,3 (Critique).
La veille de Noël, je faisais le travail le moins festif qui soit : examiner le code de sérialisation et me demander « attendez… pourquoi ceci est-il considéré comme fiable ? »
La recherche en sécurité semble souvent dramatique vue de l'extérieur. En réalité, il s'agit généralement d'une lecture minutieuse, de petites hypothèses et de l'accumulation lente de moments « c'est étrange ».
Cela a commencé comme beaucoup de choses chez Cyata : par une question simple que nous posons constamment lors de l'évaluation des stacks d'IA face au risque réel :
Où se trouvent les frontières de confiance dans les applications d'IA, et les développeurs savent-ils vraiment où se trouvent ces frontières ?
LangChain est un framework puissant et, comme la plupart des frameworks modernes, il doit déplacer des données structurées complexes : messages, appels d'outils, événements en streaming, traces, caches et « runnables ».
En examinant les recherches précédentes, il y avait déjà des travaux approfondis sur les outils et intégrations LangChain, mais très peu de découvertes dans la bibliothèque principale.
J'ai commencé la recherche en travaillant à rebours : trouver les endroits intéressants (sinks), puis déterminer comment un attaquant pouvait les atteindre. La désérialisation était une cible évidente.
Il m'a fallu pas mal de temps pour trouver quelque chose de significatif. Mais après un certain temps, j'ai découvert qu'en supposant un primitif de désérialisation contrôlé par l'attaquant, je pouvais déclencher un SSRF aveugle pouvant être utilisé pour exfiltrer des variables d'environnement (détaillé bientôt). Comme le résultat était limité à l'exfiltration de secrets et non à mon objectif principal de RCE, j'ai continué à auditer la désérialisation et j'ai pris mon temps.
Le bug n'était pas un morceau de mauvais code, c'était une absence de code. dumps() n'échappait tout simplement pas les dictionnaires contrôlés par l'utilisateur contenant des clés « lc ». Un échappement manquant dans le chemin de sérialisation, pas de désérialisation.

Il est bien plus facile de remarquer quelque chose de faux que de remarquer l'absence de quelque chose, surtout lorsque vous auditez load() plutôt que dumps(). Dans l'un des frameworks d'IA les plus minutieusement audités. Deux ans et demi.
À partir de là, la recherche est devenue un exercice structuré :
Identifier où le contenu non fiable (principalement des dictionnaires arbitraires) entre dans la sérialisation (sorties LLM, injection de prompts, saisie utilisateur, outils externes, documents extraits).
Déterminer quand ces données sérialisées sont désérialisées.
Déterminer ce qu'un attaquant peut accomplir avec une instanciation arbitraire d'objets.
À ce stade, la découverte principale était suffisamment claire et exploitable pour un rapport responsable : il y avait une brèche d'échappement dans dumps() / dumpd() autour des dictionnaires avec la clé « lc ».
L'avertissement reprend ce que nous voyons souvent en pratique : les champs comme additional_kwargs et response_metadata peuvent être influencés par la sortie LLM et l'injection de prompts, et ces champs peuvent être sérialisés-désérialisés dans de nombreux flux.
À la décharge de l'équipe LangChain : la réponse et les mesures qui ont suivi ont été décisives, ne se limitant pas à corriger le bug, mais aussi à durcir des comportements par défaut trop permissifs pour le monde dans lequel nous vivons aujourd'hui.
Le projet LangChain a décidé d'attribuer une récompense de 4 000 USD pour cette découverte. Selon huntr, la plateforme où LangChain menait son programme de récompenses, ce serait le montant le plus élevé jamais attribué dans le projet, les récompenses ayant été jusqu'ici de 125 USD au maximum.
LangChain sérialise certains objets à l'aide d'un format de dictionnaire structuré. La clé « lc » est utilisée en interne pour indiquer « ceci est une structure LangChain sérialisée », pas simplement des données utilisateur arbitraires.
C'est un modèle courant, mais il crée un invariant de sécurité : toutes les données utilisateur pouvant contenir « lc » doivent être traitées avec précaution. Sinon, un attaquant peut créer un dictionnaire qui « ressemble à » un objet interne et tromper le désérialiseur pour qu'il lui donne une valeur.
Le correctif rend l'intention explicite dans la documentation mise à jour : lors de la sérialisation, les dictionnaires simples contenant la clé « lc » sont échappés par enveloppement.
Cela évite que ces dictionnaires soient confondus avec de véritables objets LangChain sérialisés lors de la désérialisation.
Les fonctions load()/loads() de LangChain n'instancient pas des classes arbitraires – elles vérifient par rapport à une liste blanche qui contrôle quelles classes peuvent être désérialisées. Par défaut, cette liste blanche inclut des classes de langchain_core, langchain_openai, langchain_aws et d'autres packages de l'écosystème.
Voici le piège : la plupart des classes de la liste blanche ont des constructeurs inoffensifs. Trouver des chemins exploitables nécessitait de creuser dans l'écosystème à la recherche de classes qui font quelque chose de significatif lors de l'instanciation. Celles que j'ai trouvées sont détaillées ci-dessous, mais il peut y en avoir d'autres qui attendent d'être découvertes.
La fonction loads() de LangChain prend en charge le type secret, qui résout les valeurs à partir des variables d'environnement lors de la désérialisation. Avant le correctif, cette fonction secrets_from_env était activée par défaut :
if (
value.get("lc") == 1
and value.get("type") == "secret"
and value.get("id") is not None
):
[key] = value["id"]
if key in self.secrets_map:
return self.secrets_map[key]
if self.secrets_from_env and key in os.environ and os.environ[key]:
return os.environ[key] # <-- Возвращение переменной окружения
return None
Si l'objet désérialisé est renvoyé à l'attaquant, par exemple un historique de messages dans le contexte LLM, cela pourrait fuiter des variables d'environnement.
Mais le chemin le plus intéressant est l'injection de prompt indirecte. Même un attaquant qui ne peut voir aucune réponse LLM peut exfiltrer des secrets en instanciant la bonne classe. ChatBedrockConverse de langchain_aws figure à la fois dans la liste blanche par défaut de loads et effectue une requête GET lors de sa construction. Le point de terminaison GET est contrôlé par l'attaquant, et un en-tête HTTP spécifique peut être rempli avec une variable d'environnement via la fonction secrets_from_env.

Ce validateur s'exécute lorsque ChatBedrockConverse est instancié. L'attaquant contrôle endpoint_url, déclenchant une requête sortante. Combiné à secrets_from_env, l'en-tête aws_access_key_id peut être rempli avec n'importe quelle variable d'environnement – pas seulement les clés AWS.
Nous ne publions volontairement pas d'exploit prêt à l'emploi ici, afin de laisser du temps aux équipes de sécurité. Dans quelques mois, le site Huntr les publiera automatiquement.
Parmi les classes de la liste blanche par défaut de loads() figure PromptTemplate. Cette classe crée un prompt à partir d'un template, et l'un des formats de template disponibles est Jinja2.
Lorsqu'un template est rendu avec Jinja2, du code Python arbitraire peut être exécuté. Nous n'avons pas trouvé de moyen de déclencher cela directement depuis la seule fonction loads(), mais si un appel ultérieur à l'objet désérialisé déclenche le rendu, l'exécution de code s'ensuit.
Nous soupçonnons qu'il peut exister des chemins vers une exécution de code directe depuis loads(), mais nous n'en avons encore confirmé aucun. Si vous avez une idée solide ou une piste digne d'être testée, nous serions ravis d'en entendre parler – c'est précisément là que la communauté de la sécurité aide à transformer les hypothèses en preuves. 🤝
Il convient également de noter : dans les versions précédentes, la classe Chain figurait également dans la liste blanche. Cette classe avait des capacités spéciales qui pouvaient permettre un chemin vers le rendu de templates.
Votre application est potentiellement vulnérable si elle utilise des versions vulnérables de langchain-core. Voici quelques-uns des modèles vulnérables les plus courants (12 flux ont été identifiés au total) :
astream_events(version="v1") (la v1 utilise une sérialisation vulnérable ; la v2 n'est pas vulnérable)
Runnable.astream_log()
dumps() / dumpd() sur des données non fiables, suivis de load() / loads()
Désérialisation de données non fiables avec load() / loads()
Flux de sérialisation internes comme RunnableWithMessageHistory, InMemoryVectorStore.load(), certains caches, l'extraction de manifestes depuis LangChain Hub (hub.pull) et les autres composants énumérés dans l'avertissement
Néanmoins, le comportement du système est suffisamment complexe pour qu'il soit risqué de supposer qu'une revue rapide du code révélera chaque variante atteignable. Le plus sûr est de mettre à jour vers la version corrigée et de ne pas supposer que vous êtes en sécurité tant que vous ne l'avez pas fait.
L'avertissement souligne également ce que je considère comme le point le plus important pour le monde réel :
Le vecteur d'attaque le plus courant passe par les champs de réponse LLM comme additional_kwargs ou response_metadata, qui peuvent être contrôlés via l'injection de prompts puis sérialisés/désérialisés dans les opérations de streaming.
C'est exactement le type d'intersection « l'IA rencontre la sécurité classique » qui prend les organisations au dépourvu. La sortie LLM est une entrée non fiable. Si votre framework traite des fragments de cette sortie comme des objets structurés plus tard, vous devez supposer que les attaquants tenteront de les façonner.
Mettez à jour langchain-core vers la version corrigée. Si vous utilisez langchain, langchain-community ou d'autres packages de l'écosystème, vérifiez quelle version de langchain-core est réellement installée dans vos environnements de production.
Traitez additional_kwargs, response_metadata, les sorties d'outils, les documents extraits et l'historique des messages comme non fiables, sauf preuve du contraire. C'est particulièrement important si vous diffusez des logs/événements en streaming puis les réhydratez avec un chargeur.
Même après la mise à jour, suivez ce principe : n'activez pas la résolution de secrets à partir des variables d'environnement si vous ne faites pas confiance à l'entrée sérialisée. Le projet a modifié les comportements par défaut pour une raison.
Sur la base de mon rapport, il existe un avertissement étroitement lié dans LangChainJS (GHSA-r399-636x-v7f6 / CVE-2025-68665) avec des mécanismes similaires : la confusion du marqueur « lc » pendant la sérialisation, permettant l'extraction de secrets et une instanciation non sécurisée dans certaines configurations.
Si votre organisation exécute à la fois des stacks LangChain en Python et en JavaScript, considérez cela comme un rappel que le modèle se propage entre les écosystèmes : la sérialisation par marqueur, la sortie de modèle non fiable et la désérialisation ultérieure sont une forme récurrente de risque.
Nous entrons dans une phase où les frameworks d'agents IA deviennent une infrastructure critique au sein des systèmes de production. Les formats de sérialisation, les pipelines d'orchestration, l'exécution d'outils, les caches et la traçabilité ne sont plus de la « plomberie » – ils font partie de votre frontière de sécurité.
Cette vulnérabilité n'est pas « juste un bug dans une bibliothèque ». C'est une étude de cas d'un modèle plus large :
Votre application peut désérialiser des données qu'elle croit produites en toute sécurité.
Mais cette sortie sérialisée peut contenir des champs influencés par des sources non fiables (y compris les sorties LLM façonnées par l'injection de prompts).
Une seule clé réservée utilisée comme marqueur interne peut devenir un point de bascule vers des secrets et des comportements proches de l'exécution.
Chez Cyata, notre travail consiste à aider les organisations à construire visibilité, évaluation des risques, contrôle et gouvernance autour des systèmes d'IA – car si vous ne pouvez pas répondre rapidement à où les agents s'exécutent, quelles versions sont déployées et quelles données circulent à travers cela, vous volez efficacement à l'aveugle lorsque des avertissements comme celui-ci arrivent.
Si vous êtes un leader de la sécurité en train de lire ceci, voici une vérité inconfortable :
La plupart des organisations ne peuvent actuellement pas répondre, rapidement et avec assurance :
Où utilisons-nous des agents ?
Quelles versions sont déployées en production ?
Quels services ont accès à des secrets sensibles ?
Où les sorties LLM traversent-elles ces frontières ?
Ce n'est pas un « problème de développeur ». C'est un problème de visibilité et de gouvernance.
Et c'est exactement là que Cyata intervient.
Chez Cyata, nous nous concentrons sur un résultat concret : réduire le risque de l'IA et des agents sans ralentir les développeurs. Les vulnérabilités comme celle-ci sont rarement « juste un correctif ». Elles révèlent des lacunes dans la façon dont les équipes découvrent où les agents s'exécutent, comprennent les véritables frontières de confiance et garantissent des comportements par défaut plus sûrs dans des frameworks en évolution rapide.
Sachez ce qui est exécuté, où et comment c'est connecté.
Répondez rapidement à la première question du CVE : sommes-nous vulnérables, et dans quels flux ?
Détectez les runtimes d'agents et les intégrations entre environnements (IDE, CI, services, tâches planifiées, agents hébergés).
Suivez les frameworks, packages et versions utilisés.
Priorisez ce qui compte sur la base du rayon d'impact réel, pas simplement « la bibliothèque est présente ».
Permettez un triage plus rapide : ce qui est exposé à Internet, ce qui touche aux secrets, ce qui s'exécute avec des privilèges élevés.
Identifiez les chemins à plus haut risque : le contenu non fiable qui s'écoule dans des contextes privilégiés (services avec secrets, permissions d'outils étendues, accès réseau de production).
Mettez en évidence où les « champs structurés » peuvent franchir les frontières de confiance (métadonnées, sorties d'outils, événements en streaming, artefacts mis en cache).
Réduisez l'exposition avant même que chaque dépendance soit corrigée partout.
Encouragez des comportements opérationnels par défaut plus sûrs : moindres privilèges, frontières d'isolation et contrôles de politiques qui s'étendent entre les équipes.
Mettez en place des passerelles de contrôle autour des modèles risqués (par exemple : désérialisation de données non fiables, résurrection d'objets permissive, flux non sécurisés streaming-vers-cache-vers-réhydratation).
Contrôlez ou restreignez les capacités sensibles dans des contextes non fiables (par exemple : accès aux secrets depuis l'environnement, exécution d'outils à hauts privilèges ou lancement de chemins de code risqués dans des workers privilégiés).
Rendez « l'utilisation sûre des agents » reproductible, auditable et difficile à faire dériver.
Définissez des politiques pour les frameworks, versions et configurations approuvés.
Suivez et limitez dans le temps les exceptions, avec propriétaires et justifications.
Surveillez la dérive et l'utilisation risquée des fonctionnalités au fil du temps, avec une piste d'audit soutenant les revues de sécurité et la conformité.
Lorsqu'un avertissement de Noël tombe, l'objectif n'est pas l'héroïsme – c'est une réponse calme et contrôlée, appuyée par un inventaire réel et des passerelles de contrôle en place.
Rapport envoyé via Huntr – 4 décembre 2025
Reconnu par les mainteneurs de LangChain – 5 décembre 2025
Avertissement et CVE publiés – 24 décembre 2025