Introduction
Imaginez la scène : vous lancez un outil CLI pour diagnostiquer un serveur en prod. Il affiche des informations sur un process suspect... puis, sans que vous ne compreniez vraiment pourquoi, votre écran saute, des lignes disparaissent, et un prompt apparaît : "Session expirée. Merci de ressaisir votre token :"
Vous obéissez (par réflexe), vous collez un secret, et vous venez de le donner... à une illusion. Pas un malware, pas un binaire louche : juste du texte, enrichi de séquences d'échappement ANSI interprétées par votre émulateur de terminal.
C'est précisément ce qui rend les séquences d'échappement ANSI si traîtres : on a tendance à considérer la sortie terminal comme de l'affichage, alors que c'est aussi un langage de contrôle. Si un attaquant parvient à injecter ces séquences dans une chaîne qui sera imprimée (nom de service, commande de process, variable d'environnement, logs...), il peut manipuler ce que vous voyez, vous pousser à l'erreur, et parfois même interagir avec le presse‑papier.
Nous avons repéré le même problème dans l'outil open-source
witr, et nous avons ouvert une (
PR sur GitHub), pour le corriger.
Que sont les séquences ANSI ?
Les séquences ANSI (ou "ANSI escape sequences") sont des suites de caractères qui commencent généralement par le caractère ESC (Escape, 0x1B, souvent écrit \\\\x1b) et qui indiquent au terminal : "ne traite pas ça comme du texte, traite ça comme une instruction".
Elles servent à plein de choses utiles et légitimes :
Colorer du texte (rouge, vert, gras, etc.)
Déplacer le curseur
Effacer une ligne, l'écran, ou des portions d'affichage
Modifier des états (ex. mode "alternative screen"), titres de fenêtre, etc.
La forme la plus connue est la séquence CSI (Control Sequence Introducer) qui ressemble à :
ESC + [ + paramètres + lettre de commande
Quand on représente ça en string, on verra souvent \\\\x1b[ ... m (le m étant typique des styles/couleurs)
Exemple simple : mettre du texte en rouge puis revenir au style normal :
\\\\x1b[31m → rouge
\\\\x1b[0m → reset
Exemple avec un changement de couleur
Contrôle d'affichage
Certaines séquences permettent d'effacer une ligne ou de repositionner le curseur (très utilisé par les barres de progression). C'est pratique... et exactement le genre de mécanisme qui devient dangereux si la chaîne affichée est contrôlée par un tiers.
Le risque de sécurité
Une vulnérabilité terminal liée aux séquences ANSI ne permet pas forcément une exécution de code à distance qui fonctionne à tous les coups. Le plus souvent, c'est une attaque de manipulation : ce que l'utilisateur voit n'est plus la réalité. Et en DevSecOps, voir faux suffit souvent à prendre une mauvaise décision.
Voici les scénarios les plus courants.
1) Spoofing visuel : faux prompts, fausses erreurs, fausses validations
Si un outil CLI imprime des valeurs non fiables (ex. nom de process, arguments, env vars, champs provenant d'une API), un attaquant peut injecter des séquences pour :
masquer un avertissement,
effacer une ligne gênante pour l’attaquant,
déplacer le curseur et réécrire une sortie,
produire un affichage qui ressemble à un prompt système.
Le piège : l'utilisateur croit interagir avec une demande légitime (sudo, token, login...), alors que c'est juste une illusion imprimée par un programme. Et les humains sont des parseurs très tolérants.
Ce type de problématique a été observé à grande échelle dans l'écosystème K8s/OpenShift : l'injection de séquences ANSI dans des champs affichés au terminal a été documentée (notamment autour de la
CVE-2021-25743) dans des recherches sur l'abus des émulateurs de terminal.
2) Log poisoning : quand vos logs deviennent une arme
Quand on parle d’attaques liées au terminal, on imagine presque toujours un scénario de SSH compromis, avec un attaquant en face. Mais le vecteur le plus sournois, c'est le logging :
logs d'applications affichés en console,
logs CI/CD,
journaux d'observabilité,
traces imprimées dans des dashboards qui finissent... copiés/collés dans un terminal.
Un exemple récent et très concret : la
CVE-2025-58160 dans
tracing-subscriber (Rust). Des entrées non fiables pouvaient injecter des séquences ANSI dans la sortie terminal via les logs, permettant de manipuler l'affichage (titres, effacement, etc.) et de tromper l'opérateur.
3) Presse‑papier, titres, liens cliquables : l'attaque "à retardement"
Certaines familles de séquences (notamment autour de ce qu'on appelle "OSC" dans beaucoup de terminaux) permettent d'interagir avec des fonctionnalités “OS-like" :
titre de fenêtre,
liens cliquables (phishing via URL trompeuse),
et dans certains environnements : interaction avec le presse‑papier.
Un cas d'école récent à connaître : Apache Tomcat n'échappait pas les séquences ANSI dans certains messages de log. Si Tomcat tournait dans une console (notamment sur Windows avec support ANSI), un attaquant pouvait injecter des séquences via une URL spécialement conçue afin de manipuler la console et le clipboard, et tenter de pousser un admin à exécuter une commande contrôlée par l'attaquant (attaque de type social engineering).
4) Pourquoi c'est si fréquent dans les CLI ?
Parce que les outils CLI affichent des informations fournies par le système d’exploitation ou par d’autres processus, dont une partie peut être contrôlée ou influencée par un attaquant.
lignes de commande (argv) de processus,
variables d'environnement,
noms de services,
champs provenant d'API (K8s, cloud, orchestrateurs),
noms de fichiers (parfois contrôlables),
messages de logs (souvent contrôlables).
Dans la PR que nous avons ouvert, nous résumons le problème : des chaînes "user-controlled / system-derived" peuvent contenir des caractères de contrôle, dont des escapes ANSI, et les imprimer tel quel peut altérer l'affichage, effacer des informations, ou impacter le presse‑papier.
witr, c'est quoi ?
witr ("Why is this running?") est un outil CLI orienté diagnostic : il sert à expliquer pourquoi un process/service existe et quelle chaîne de causalité (systemd, conteneur, shell, cron, etc.) le maintient en vie, avec une sortie "narrative" et lisible.
Concrètement, witr agrère et affiche des informations issues du système : ligne de commande des processus, relations parent/enfant et contexte d’exécution dans le but de donner à l’administrateur système une vision compréhensible de l’origine d’un service, sans avoir à croiser manuellement plusieurs commandes (ps, systemctl, docker, etc.).
Le problème identifié
Le cœur du sujet : witr imprime beaucoup de données issues du système (process command lines, env vars, service names...). Or ces données peuvent être influencées par un attaquant local (ou par une supply chain / un job malveillant / un conteneur compromis), et contenir des séquences ANSI.
La stratégie de correction
La correction ne repose pas sur un simple patch, mais sur un choix d’architecture côté affichage, ce qui mérite un léger détour technique.
Plutôt que d'espérer que chaque fmt.Printf(...) soit correctement “sanitizé” par chaque futur maintainer, la PR adopte une approche plus durable : centraliser la protection au point d’écriture vers le terminal.
L'idée est simple mais structurante : introduire un writer "safe" qui sanitize systématiquement tout ce qui est écrit vers la sortie standard, et n'autoriser les séquences ANSI que quand elles sont explicitement déclarées comme fiables (ex. les couleurs ajoutées volontairement par l'outil).
Dans la codebase, cette solution s'implémente avec 3 briques:
un SafeTerminalWriter qui implémente io.Writer et neutralise les caractères de contrôle dangereux,
un wrapper Printer autour des fonctions d'impression afin d’éviter l’usage direct de fmt.Print*
et un mécanisme "escape hatch" (via un type ansiString) pour les séquences ANSI maîtrisées par l'application (comme les couleurs).
La solution technique
Objectif : empêcher qu'une chaîne non fiable soit interprétée comme une instruction terminal.
Il existe deux approches complémentaires :
Neutraliser / échapper les caractères de contrôle (dont ESC)
N'autoriser les ANSI que via un canal de confiance (ex. une fonction de coloration interne)
Avant / après (exemple simplifié)
Avant : impression directe d'une chaîne issue du système
fmt.Fprintf(os.Stdout, "Command: %s\\\\n", cmdline)
Après : sanitation centralisée
func SanitizeForTerminal(s string) string {
out := make([]rune, 0, len(s))
for _, r := range s {
if r == '\\\\n' || r == '\\\\t' {
out = append(out, r)
continue
}
if r < 0x20 || r == 0x7f {
out = append(out, '\\\\x1b')
continue
}
out = append(out, r)
}
return string(out)
}
fmt.Fprintf(os.Stdout, "Command: %s\\\\n", SanitizeForTerminal(cmdline))
Les exemples précédents illustrent le principe, mais ils restent volontairement simplifiés.
Dans un CLI réel, utilisé en production et amené à évoluer, la protection contre les séquences ANSI doit être plus fine et plus systématique
filtrer spécifiquement ESC (0x1b) et certaines séquences,
reconnaître et escape les principales de séquences ANSI (CSI, OSC, etc.)
et surtout envelopper l’écriture vers le terminal dans un composant dédié (via un safe writer), afin qu’un oubli ponctuel ne réintroduise pas la vulnérabilité.
Côté écosystèmes, il existe des bibliothèques utiles :
JS/TS : détection/filtrage via des libs type ansi-regex, suppression via strip-ansi, génération contrôlée via ansi-escapes
Python : sanitation via re + allowlist, ou wrappers d'output
Go : mapping de runes + writers (io.Writer) sécurisés
Bonnes pratiques et conclusion
S’il ne fallait retenir qu’un point, c’est que la sortie d’un terminal fait partie intégrante de la surface d’attaque d’un outil CLI. Toute donnée affichée sans contrôle, qu’elle provienne d’un utilisateur, d’un service ou du système, doit être traitée comme non fiable.