La liste blanche qui n'en est pas une : test de sous-chaîne dans un garde-fou d'outil LLM

Sécurité de l'IA Niveau 3/4 ~5 min 18 septembre 2026

Le défi

Atlas est l'assistant que Halcyon fait tourner pour ses propres équipes. Il dispose d'un seul outil qui touche au réseau : fetch_url, pour lire une page qu'un collègue lui envoie avant de répondre à son sujet. L'équipe savait que donner un client HTTP à un modèle, c'est la recette du server-side request forgery, alors elle a écrit un garde-fou avant la mise en production. Seuls trois domaines de documentation sont joignables. Seulement http et https. Une liste de blocage couvre l'adresse de métadonnées cloud et la boucle locale. Le code est passé deux fois en revue. Il ne tient toujours pas. L'une des trois vérifications accepte des hôtes que personne chez Halcyon ne possède, et quiconque parvient à placer un lien devant Atlas peut diriger l'outil vers une infrastructure qu'il contrôle. Lisez les trois fichiers et nommez la fonction dont la vérification laisse passer des hôtes contrôlés par l'attaquant.

Ce que tu vas apprendre

  • Distinguer un test d'appartenance d'un test de sous-chaîne en Python
  • Expliquer pourquoi la correspondance par sous-chaîne est sûre dans une liste de blocage et dangereuse dans une liste blanche
  • Construire un nom d'hôte qui contourne une liste blanche par sous-chaîne
  • Décrire comment un outil d'agent transforme une SSRF en sortie lisible pour l'attaquant
  • Écrire une comparaison d'hôte qu'un suffixe possédé par l'attaquant ne peut pas élargir

Compétences testées

Revue de code sourceRaisonnement sur les SSRFSécurité des outils d'agent IA

Prérequis

  • Lire des fonctions et des compréhensions Python
  • Savoir ce que sont un nom d'hôte et un sous-domaine

Comment ça marche

Donner un client HTTP à un modèle, c'est lui donner un forgeur de requêtes. Le modèle décide de l'URL, le serveur effectue la requête, et le serveur se trouve à l'intérieur de votre réseau. Toute la propriété de sécurité d'un outil fetch_url tient donc en une seule question : cet hôte est-il un de ceux que nous avons choisi d'autoriser ?

Atlas répond à cette question dans host_allowed, et répond par accident à une question différente. any(domain in host for domain in ALLOWED_DOMAINS) ressemble à un test d'appartenance, et dans scheme_allowed, le même mot-clé en est vraiment un, car urlparse(url).scheme in ("http", "https") compare aux éléments d'un tuple. Entre deux chaînes, in signifie sous-chaîne. host_allowed demande si le texte docs.halcyon.example apparaît quelque part dans l'hôte.

Un attaquant qui possède un domaine quelconque possède un nombre infini d'hôtes contenant ce texte. docs.halcyon.example.attacker.example est un sous-domaine tout à fait ordinaire de attacker.example. Cela ne coûte rien, cela résout vers l'adresse de son choix, et cela satisfait la vérification. Enfouir le nom autorisé au milieu fonctionne tout aussi bien.

La liste de blocage quelques lignes plus haut est elle aussi un test de sous-chaîne, et celle-là n'est pas un bug. Élargir une liste de blocage la fait attraper davantage, donc any(bad in url.lower() for bad in BLOCKED) refuse plus d'URL que prévu plutôt que moins. Le sens compte : une correspondance laxiste est dangereuse quand elle autorise, et simplement maladroite quand elle refuse.

Ce qui rend cela pire qu'une SSRF aveugle, c'est que l'agent fait un retour. Le corps de la réponse est ajouté à la conversation comme sortie d'outil, puis résumé à celui qui parle avec Atlas, si bien que l'attaquant n'a pas besoin de canal auxiliaire pour lire ce que le serveur a récupéré.

Erreurs fréquentes

  • Répondre scheme_allowed. Elle utilise le même mot-clé mais compare à un tuple, donc c'est un véritable test d'appartenance et elle est correcte.
  • Répondre target_allowed. Son ordre et sa couverture sont corrects. Elle échoue uniquement parce que la fonction auxiliaire à laquelle elle fait confiance est erronée.
  • Désigner la liste de blocage comme le bug. La correspondance par sous-chaîne dans une liste de blocage bloque trop, ce qui est le sens sûr.
  • Accuser allow_redirects. Il est explicitement défini à False, ce qui est la seule chose que les auteurs ont bien faite, et pour la bonne raison.
  • Penser que l'adresse de métadonnées est nécessaire. N'importe quel hôte attaquant suffit déjà, car le corps récupéré revient au modèle.

Comment s'en protéger

Comparez les hôtes comme le fait le DNS : par étiquettes entières, avec une égalité.

  • Analysez une seule fois avec urlparse, prenez hostname plutôt que netloc pour que les identifiants et les ports soient déjà retirés, mettez en minuscules et supprimez un point final.
  • Testez-le contre un set avec == ou in sur cet ensemble. Si des sous-domaines doivent être autorisés, vérifiez host == d or host.endswith("." + d), jamais une simple sous-chaîne.
  • Résolvez le nom d'hôte et rejetez la requête si l'adresse est privée, loopback, link-local ou multicast, puis connectez-vous à l'adresse que vous avez validée pour que le DNS ne puisse pas changer entre-temps.
  • Désactivez les redirections, ou réexécutez le garde-fou complet à chaque saut.
  • Filtrez le trafic sortant du conteneur dans lequel tourne l'agent. La liste blanche est le premier contrôle, pas le seul.

Solution complète

Les membres Pro et Max débloquent la solution complète étape par étape.

Passer Pro

Statistiques de la communauté

125 résolutions
75% taux de réussite
M2F14M3 Premier sang

Hacks du jour associés

Contributeurs

Contributeurs crédités

Ces hackers ont signalé des problèmes, amélioré le contenu et renforcé cette page.

27 000+ Hackers 100+ Labs & Cours Gratuit
Commencer Gratuitement