Retour à tous les articles
Publié le · par Renaud Deraison

Le bac à sable a ouvert le port

CVE-2026-65105 est un bug réseau dans NVIDIA NemoClaw, un outil dont toute la raison d'être est de faire tourner un agent IA dans un bac à sable. Ce bac à sable est un conteneur Docker, un conteneur ne peut pas atteindre l'interface de bouclage de l'hôte, alors NemoClaw a lancé le serveur de modèle local sur toutes les interfaces — ce qui a désactivé la seule vérification qui aurait empêché une page web de lui parler. Ce qu'une page de passage a alors pris, c'est le gabarit de conversation du modèle, la couche qui met en forme chaque prompt système avant que le modèle ne le lise. Dans Bromure Agentic Coding, le moteur local écoute sur le bouclage, sur un port que l'invité n'apprend jamais, et l'invité l'atteint par vsock plutôt que par le réseau.

C'est le confinement qui a ouvert la brèche. Un bac à sable incapable d'atteindre l'interface de bouclage de votre machine pousse le serveur de modèle sur toutes les interfaces, et à partir de là un onglet de navigateur peut l'atteindre lui aussi.

Vous faites tourner votre agent de codage avec toutes les précautions. Il vit dans un bac à sable. Le modèle avec lequel il pense tourne sur votre propre machine, donc aucun prompt ne quitte les lieux. Vous avez fait les deux choses que tout le monde vous recommande de faire.

Puis vous ouvrez un onglet de navigateur, et à partir de cet instant votre agent se met à écrire du code comportant un défaut qu'il se garde de mentionner.

C'est la CVE-2026-65105, divulguée le 25 août par Elad Luz et Ofek Itach chez Oasis Security, dont l'analyse parle de détournement d'agent au passage. Ils sont passés par le PSIRT de NVIDIA avant de publier, et l'affaire est sortie le jour même dans The Hacker News, SiliconANGLE et CSO Online. NVIDIA a publié un avis de sécurité et un correctif.

Le produit s'appelle NemoClaw, l'outil de NVIDIA pour déployer un agent OpenClaw dans un bac à sable OpenShell avec un backend Ollama local pour l'inférence. Le confinement, c'est tout l'argument de vente.

Le conteneur ne pouvait pas atteindre le bouclage

Ollama écoute sur le port 11434 et, par défaut, il se lie à 127.0.0.1 — l'interface de bouclage, joignable uniquement depuis la machine elle-même.

Les bacs à sable OpenShell sont des conteneurs Docker. Un conteneur possède son propre espace de noms réseau, donc 127.0.0.1 à l'intérieur du conteneur désigne le conteneur, pas l'hôte. L'agent dans le bac à sable ne peut donc pas atteindre un serveur de modèle lié au bouclage de l'hôte.

NemoClaw a résolu ça en une variable d'environnement. Il lance Ollama avec OLLAMA_HOST=0.0.0.0:11434 : toutes les interfaces de la machine, y compris celle qui donne sur le wifi du bureau. L'installateur continuait d'afficher Using Ollama on localhost:11434 pendant que la socket répondait au monde entier.

Vient ensuite l'étape qui transforme un choix de configuration en vulnérabilité. L'API d'Ollama n'a pas d'authentification, elle porte donc deux garde-fous à la place. Le premier est une vérification CORS sur l'en-tête Origin. Le second est une liste blanche sur l'en-tête Host qui accepte localhost, le nom d'hôte de la machine, et les noms se terminant par .localhost, .local ou .internal.

Le second garde-fou comporte une exception. Ollama regarde l'adresse à laquelle il est lié, et si cette adresse n'est pas le bouclage, il saute entièrement la vérification du Host. On devine pourquoi quelqu'un a écrit ça : un opérateur qui se lie à une interface publique veut des clients distants, et une liste blanche de Host conçue pour le bouclage les renverrait tous. Résultat : le réglage qui expose le port est le même réglage qui désarme la vérification qui le protège.

Il ne reste que la vérification CORS, et le DNS rebinding traverse CORS par construction. L'attaquant sert sa page depuis un domaine qu'il contrôle, sur le port 11434, puis fait re-résoudre ce domaine vers 127.0.0.1. Origin et Host sont désormais le même nom d'hôte choisi par l'attaquant, ce qu'Ollama lit comme une même origine. Nous avons détaillé le mécanisme du rebinding la semaine dernière, alors prenons-le comme acquis : une page que vous visitez devient un client d'un service sur votre machine, et cela prend environ une minute.

Le rebinding est le chemin d'entrée intéressant. C'est aussi le plus difficile. Un serveur sur 0.0.0.0 répond à tous les autres appareils du segment réseau sans la moindre astuce de navigateur : le portable de l'invité, la télé connectée, la machine compromise deux bureaux plus loin.

la contraintebac à sable OpenShell = conteneur Dockerson propre espace de noms réseau, donc127.0.0.1 = le conteneur, pas l'hôtel'agent n'atteint pas le serveur de modèleun vrai problème, qui appelait une vraie réponsela réponse livrée par NemoClawOLLAMA_HOST=0.0.0.0:11434toutes les interfaces, aucune authentificationl'installateur affiche : « Using Ollama on localhost »les deux garde-fous d'Ollama1 · vérification CORS sur l'en-tête Origin2 · liste blanche Host : localhost, nom d'hôte,.localhost .local .internalle 2 saute si la liaison n'est pas le bouclagedeux chemins d'entréeune page : rebinder le domaine vers 127.0.0.1,Origin et Host coïncident, CORS passele LAN : composer le port, sans astuceles deux arrivent sur l'API entière, sans authLa configuration qui a exposé le port est la même que celle qui a désactivé la vérification censée le protéger.
Pourquoi le port a bougé. Le bac à sable est un conteneur Docker, il ne peut donc pas atteindre l'interface de bouclage de l'hôte, et NemoClaw a réglé ça en liant le serveur de modèle à toutes les interfaces. Ollama saute sa vérification de l'en-tête Host dès que l'adresse de liaison n'est pas le bouclage, ce qui ne laisse qu'une vérification CORS — et le DNS rebinding fait d'Origin et de Host le même nom choisi par l'attaquant.

Ils ne voulaient pas d'exécution de code

Atteindre l'API, c'est la moitié ennuyeuse. Ce qu'Oasis a fait une fois dedans, voilà la partie neuve.

Le geste évident consiste à écrire un prompt système malveillant dans le modèle. Ça échoue ici : OpenClaw envoie son propre prompt système dans le tableau messages à chaque requête, et le prompt du client l'emporte sur celui stocké dans le modèle. Les chercheurs sont donc descendus d'une couche.

L'endpoint /api/create d'Ollama accepte un champ template. Un gabarit de conversation est un template Go qui transforme le tableau structuré messages — rôles, contenus, définitions d'outils — en l'unique bloc de texte brut que le modèle lit réellement. Il s'exécute au moment de l'inférence, après que le client a remis ses messages, et il les touche tous.

L'attaque tient en trois requêtes. Lire le vrai gabarit avec /api/show. Garder son rendu des outils et ses jetons spéciaux exactement tels quels, et ne changer que la façon dont les messages système sont rendus, en y ajoutant l'instruction de votre choix. Le réécrire avec /api/create.

Le client continue d'envoyer son prompt système soigneusement rédigé. Le gabarit enveloppe ce prompt dans celui de l'attaquant, à chaque fois, avant que le modèle n'en voie le premier jeton. Selon les mots d'Elad Luz, cités par CSO Online, l'instruction se loge « une couche en dessous de tout ce qu'un garde-fou ou un opérateur peut voir ».

La modification atterrit dans la définition du modèle sur le disque, elle survit donc à la conversation, à la réinitialisation et au redémarrage. Les métadonnées du modèle, sa taille et ses capacités affichées restent identiques, et rien ne paraît différent dans une liste de modèles installés. Tous les consommateurs de ce modèle en héritent : la CLI que vous tapotez à la main, et l'agent qui tourne pendant que vous déjeunez.

Oasis énumère ce à quoi une telle instruction peut servir. Rien là-dedans n'est assez spectaculaire pour se faire remarquer.

Glisser le bug dedans

Produire du code portant un défaut subtil — le genre qui se lit très bien en revue, parce qu'il a été écrit pour bien se lire en revue.

Se taire

Ne jamais signaler un problème de sécurité. Un agent qui vous alertait sur quelque chose et ne le fait plus, c'est difficile à repérer.

Orienter le choix

Recommander tel paquet, telle URL, telle configuration. Vous demandiez une suggestion ; vous en avez eu une.

Tout renvoyer

Là où l'agent a un accès réseau, expédier le contenu des conversations et des fichiers vers un endpoint choisi par l'attaquant.

Le reste de l'API non authentifiée fait de la figuration : /api/generate et /api/chat pour faire tourner de l'inférence sur le GPU de quelqu'un d'autre, /api/pull pour remplir un disque, /api/delete pour détruire les modèles, /api/push pour publier sous le compte ollama.com de la victime, et /api/me pour apprendre le nom d'hôte, la clé publique et l'identifiant connecté de celui sur qui vous venez d'atterrir.

Le bac à sable n'a jamais été le rayon d'explosion

Oasis est précis sur ce que le bac à sable a fait et n'a pas fait. Le bac à sable OpenShell fonctionne : il isole le système de fichiers, le réseau et les processus, et il garde le poste propre. Un agent dans une organisation gagne sa place en détenant le contrôle de version, la CI/CD, des API internes, des comptes cloud, la messagerie et une pile de serveurs MCP. Prenez l'agent et vous dirigez cet accès. Le résumé de l'analyse est que le vrai rayon d'explosion suit les ressources que l'agent est autorisé à atteindre, pas la frontière de son bac à sable.

Ce qui est la question à laquelle un conteneur ne répond jamais pour vous, et ici elle arrive avec une torsion. C'est la couche de confinement qui a créé l'exposition. L'espace de noms réseau de Docker a fait son travail. Le contourner a mis le serveur de modèle sur le wifi.

Où vit le modèle local dans un profil

Bromure Agentic Coding fait tourner de l'inférence locale lui aussi. Les agents d'un profil peuvent être dirigés vers un modèle embarqué plutôt que vers un fournisseur cloud, c'est exactement la fonctionnalité que propose NemoClaw. La plomberie en dessous est construite à l'envers de la leur, et chaque étape répond directement à quelque chose de ce qui précède.

Le moteur écoute sur le bouclage, jamais sur 0.0.0.0. Le serveur de modèle embarqué tourne sur le Mac, sur 127.0.0.1, sur un port que le noyau attribue au lancement plutôt qu'un numéro fixe. Le port dynamique est là pour une raison terne : que Bromure ne se batte jamais avec ce qui occupe déjà le traditionnel 11434 sur ce Mac, Ollama et LM Studio compris. Cela signifie aussi qu'il n'y a aucun port bien connu qu'une page puisse viser.

L'invité ne l'atteint pas par le réseau. Dans la VM, l'agent compose 127.0.0.1:11434, l'adresse qu'il attend. Cette connexion ne s'approche d'aucune route. Un pont dans la VM raccorde le flux TCP sur vsock, le transport socket virtio, sur le port 8446, et l'extrémité hôte du canal le remet au moteur en bouclage. Un canal vsock est un tuyau entre un hyperviseur et un invité. Il n'a ni adresse IP, ni nom d'hôte, ni enregistrement DNS : il n'y a rien à rebinder, rien à scanner, et aucun moyen pour un navigateur de le composer. L'invité n'apprend jamais sur quel port se trouve le moteur.

Rien sur votre LAN ne peut atteindre la VM. Sous Resources → Network, le mode par défaut est NAT : la sortie fonctionne, et rien sur votre réseau local ne peut atteindre la VM. Cela ferme la moitié de la découverte NemoClaw qui n'a besoin d'aucun rebinding, l'appareil sur le même wifi qui compose le port directement, et cela le ferme sans que vous changiez un réglage.

Il n'y a pas de définition de modèle dans la VM à réécrire. Les agents invités sont épinglés à un unique identifiant de modèle, bromure-local, et ils l'adressent à bromure.llm, un nom d'hôte synthétique sans aucun enregistrement DNS derrière. Le proxy dans la VM intercepte ce nom et le transmet à l'hôte. Quel modèle répond est un mapping que vous tenez côté hôte, dans le panneau Local Models du profil, et le changer est un remappage côté hôte : aucune reconfiguration dans la VM, aucun redémarrage d'agent. Le gabarit, les poids et le routage vivent tous sur le Mac. Rien dans l'invité ne détient d'équivalent à /api/create, parce que rien dans l'invité ne détient le modèle.

L'inférence locale passe par le même pipeline que le cloud. Parce que ce trafic passe par le proxy de Bromure plutôt que directement par un port, il reçoit le même traitement qu'un appel à Anthropic ou OpenAI : la même trace, la même journalisation et la même détection d'injection de prompt. Le panneau Prompt Injection note le contenu des fichiers, les pages web et les sorties d'outils que l'agent lit, avec un modèle PromptGuard local, et note les fichiers CLAUDE.md, AGENTS.md et GROK.md qu'il charge comme faisant autorité, avec un classifieur ModernBERT affiné plus un scanner déterministe des charges en Unicode invisible. Les détections partent dans le Security Log, ou mettent la requête en pause, ou renvoient un 451 sec, selon celle des trois réponses que vous avez choisie.

Un port sur toutes les interfacesoù le moteur écoute-t-il ?0.0.0.0:11434 · fixe, bien connucomment le bac à sable l'atteint-il ?par IP, comme tout le monde sur le wifiqui d'autre peut l'atteindre ?une page rebindeuse, et le LAN, sans authqui peut réécrire le gabarit ?tout ce qui atteint le port, viaPOST /api/createUn canal sans adresseoù le moteur écoute-t-il ?127.0.0.1:<port du noyau> · hôte seulcomment la VM l'atteint-elle ?vsock 8446 — un tuyau, pas un réseauqui d'autre peut l'atteindre ?personne : ni IP, ni nom d'hôte, ni DNSqui choisit le modèle ?vous, dans le panneau Local Models du profil ;l'invité ne connaît que le nom bromure-local
Deux façons de donner un modèle local à un agent. À gauche, un port HTTP sur toutes les interfaces, sans authentification, avec derrière une définition de modèle modifiable — joignable par n'importe quel appareil du réseau et par n'importe quelle page qui rebinde un nom d'hôte. À droite, un moteur en bouclage sur un port attribué par le noyau, atteint depuis l'invité par un canal vsock qui n'a aucune adresse à composer, avec le choix du modèle tenu sur l'hôte.

Maintenant, supposez que le modèle vous ment

Poussez un argument d'architecture assez loin et quelqu'un devrait le mettre à l'épreuve. Alors offrez la victoire à l'attaquant par le chemin qui vous plaît : un gabarit empoisonné, un mauvais jour chez un fournisseur de modèles, une injection de prompt qui est passée. Votre agent travaille désormais pour quelqu'un d'autre et sa production se lit toujours aussi bien. Faites passer ces quatre charges utiles dans un profil.

Orienter le choix, et le choix se fait quand même juger. Cette charge utile s'appuie le plus sur l'autorité du modèle, et c'est elle qui rencontre le plus de mur. Bromure évalue chaque récupération de paquet au niveau du proxy hôte avant que l'agent ne voie la réponse, sur npm, PyPI, Cargo, RubyGems, Maven, NuGet, Go modules et Packagist, et le proxy ne demande pas qui a suggéré le paquet. Une barrière d'âge de deux jours est active par défaut, ce qui traite l'essentiel de ce dont a besoin un typosquat fraîchement publié. Activez la recherche OSV, pointez le filtrage de paquets vers socket.dev ou Delpi, retirez les scripts d'installation, et une recommandation doit survivre à quatre avis sur lesquels le modèle n'a pas voix. Les fichiers .npmrc et pip.conf dans la VM peuvent resserrer ces réglages. Ils ne peuvent pas les desserrer.

Tout renvoyer, sur un fil que vous contrôlez. L'exfiltration a besoin de sortie réseau. Guardrails → Outbound connections est une table de règles à la pf — une action, un protocole (tcp, udp, web, any), un hôte ou un CIDR, une liste de ports, et pour web une liste de méthodes HTTP — évaluée de haut en bas, la première correspondance gagne, avec Unmatched traffic sur Allow ou Deny. Mettez-le sur Deny, listez les hôtes dont votre travail a besoin, et l'endpoint de l'attaquant n'en fait pas partie. L'application se fait au commutateur virtuel et à la couche SNI sur l'hôte, donc rien de ce que l'invité fait à son propre routage ne change le verdict.

Glisser le bug dedans, puis essayer de le livrer. Guardrails porte aussi un mode pour chaque service que l'agent peut atteindre : Block destructive ou Read-only sur GitHub, GitLab, Bitbucket, AWS, Kubernetes, DigitalOcean, les registres de conteneurs et chaque endpoint de base de données configuré. En mode lecture seule, un git push est classé comme écriture et bloqué ; un fetch est toujours autorisé. Le commit piégé reste dans la VM.

Et l'accès dont il hérite est faux. Le point d'Oasis sur le rayon d'explosion est celui auquel un profil consacre le plus d'efforts. Les identifiants dans la VM sont des leurres : des paramètres fictifs brm_… en variables d'environnement, un ~/.kube/config synthétique avec des certificats client jetables, un faux blob base64 dans ~/.docker/config.json, et des requêtes AWS re-signées sur l'hôte, si bien que tout ce qui contourne le proxy obtient InvalidSignatureException plutôt qu'un accès. Les clés SSH privées ne sont jamais dans la VM — l'hôte signe via un agent propre à chaque profil. Activez Require approval to use pour un identifiant donné et chaque substitution devient une boîte de dialogue côté hôte avec une autorisation bornée dans le temps : cinq minutes, une heure, le reste de la session. Le partage de dossiers est plafonné à huit répertoires que vous choisissez à la main, donc le reste de votre Mac n'est pas là pour être lu.

Et vous voyez tout ça. La fenêtre Security Log (Window → Security Log…) est une unique table chronologique côté hôte : verdicts sur les paquets, chaque destination ouverte par la VM et si elle a été autorisée ou refusée, échanges d'identifiants, détections d'injection. Le code invité ne peut pas la modifier, parce que le code invité ne peut pas l'atteindre. En fin de journée, Erase home… et Reset to base… sous Resources → Storage ramènent le profil à son état d'après clonage.

Passez à l'action

Si vous faites tourner NemoClaw, prenez le correctif de NVIDIA — il ramène Ollama sur le bouclage et place un proxy à jeton devant tout ce qui doit légitimement en sortir. The Hacker News note que le chemin Windows et WSL n'est pas entièrement couvert par cette version, alors vérifiez votre plateforme dans l'avis plutôt que de supposer. Ensuite, allez trouver tous les autres serveurs de modèles sur votre machine et demandez-leur à quelle adresse ils sont liés. La réponse est plus souvent 0.0.0.0 qu'on ne le croit, parce que les conteneurs n'arrêtent pas de le réclamer.

Dans un profil, les réglages qui valent deux minutes sont la courte liste habituelle. Guardrails → Outbound connections avec Unmatched traffic sur Deny. Credentials → Require approval to use sur tout ce qui peut dépenser de l'argent ou détruire des données. Supply Chain → OSV vulnerability check et le filtrage socket.dev ou Delpi si vous avez une clé, par-dessus la barrière d'âge déjà active. Prompt Injection → les deux détecteurs, pour que le trafic vers votre modèle local soit lu aussi attentivement que celui vers un modèle cloud.

Continuez à faire tourner le modèle sur votre propre machine. Ce choix vous achète des prompts qui restent locaux, et il vaut la peine. Il ne dit rien sur qui d'autre a le droit de parler au serveur qui les détient, ce qui est une question de socket, et une socket mérite une meilleure réponse qu'une variable d'environnement réglée pour faire plaisir à un conteneur. Installez Bromure Agentic Coding, choisissez votre modèle sur l'hôte, et laissez à l'invité un tuyau plutôt qu'un port.