
XSS stocké de Bloomberg Memray via des métadonnées de ligne de commande non échappées
XSS stocké de Bloomberg Memray via des métadonnées de ligne de commande non échappées
J'ai trouvé ce problème en examinant Memray, le profileur mémoire Python de Bloomberg, avec une question simple en tête :
Des métadonnées d'exécution contrôlées par un attaquant peuvent-elles passer de manière non sécurisée dans la sortie d'un rapport rendu par le navigateur ?
Dans ce cas, la réponse était oui.
Le bug se trouvait dans le chemin de génération des rapports HTML de Memray, où les métadonnées de ligne de commande étaient rendues dans un rapport ouvert dans le navigateur sans échappement. Cela a transformé un champ opérationnel en sink HTML exécutable et a finalement abouti à la CVE-2026-32722.
Projet : Memray sur GitHub
Avis de sécurité : GHSA-r5pr-887v-m2w9 / CVE-2026-32722
attacker-controlled argv → metadata.command_line → HTML template sink without escaping → raw markup in generated report → browser-side JavaScript execution
Memray est un profileur mémoire Python.
Il instrumente un processus Python, enregistre le comportement d'allocation et produit des rapports qui aident les développeurs à comprendre :
Certains de ces rapports sont générés en HTML et ouverts dans un navigateur.
Cela fait de la génération de rapports une véritable frontière de sécurité.
La question pertinente n'est pas de savoir si Memray est un « outil local ».
La question pertinente est de savoir si des données contrôlées par un attaquant peuvent passer de manière non sécurisée dans la sortie rendue par le navigateur.
Dans ce cas, c'était possible.
Beaucoup de gens sous-estiment les outils de développement.
C'est une erreur.
Dès lors qu'un outil :
il hérite des mêmes risques d'encodage de sortie qu'une application web.
C'était le problème central ici.
Ce bug n'était pas dans la logique de profilage. Il n'était pas dans le suivi des allocations. Il n'était pas dans la gestion native de la mémoire.
C'était un échec classique de frontière de confiance :
C'est suffisant pour créer une véritable vulnérabilité.
Je n'ai pas abordé Memray en fuzzant des options CLI aléatoires ou en cherchant des crashs.
L'approche plus solide consistait à identifier d'abord la surface de sécurité la plus probable.
Pour Memray, c'était la génération de rapports HTML.
Pourquoi ?
Parce que la sortie HTML introduit un sink navigateur, et les sinks navigateur transforment les bugs de métadonnées ordinaires en problèmes de sécurité si :
C'est exactement ce qui s'est passé ici.
Le bug se résume à deux lignes.
Dans :
def get_render_environment() -> jinja2.Environment:
loader = jinja2.PackageLoader("memray.reporters")
env = jinja2.Environment(loader=loader)
l'environnement Jinja est créé sans autoescape.
Puis dans :
Command line: <code>{{ metadata.command_line }}</code><br>
metadata.command_line est rendu directement dans le HTML.
C'est toute la vulnérabilité.
Parce que metadata.command_line est influencé par un attaquant.
Memray enregistre la ligne de commande utilisée pour exécuter le programme profilé.
Cela signifie que les valeurs contrôlées par l'utilisateur provenant de argv sont conservées comme métadonnées puis insérées dans le rapport.
La chaîne d'exploitation est donc simple :
metadata.command_lineCela transforme des métadonnées en contenu navigateur exécutable.
La distinction importante est l'exécution.
Beaucoup de bugs produisent du HTML malformé. Cela ne suffit pas à lui seul.
Ici, le contenu contrôlé par un attaquant n'était pas simplement visible dans le source de la page. Il a été interprété par le navigateur comme du HTML actif et exécuté comme JavaScript.
C'est la différence entre :
La question n'était donc pas :
« Est-ce que du HTML peut apparaître dans le rapport ? »
La vraie question était :
« Est-ce que du HTML contrôlé par un attaquant peut devenir exécutable lorsque le rapport est ouvert ? »
La réponse était oui.
Mon premier reproducteur utilisait un script explicite plus un argument contrôlé par l'attaquant :
cat > victim.py <<'PY'
x = [b"A" * 1024 for _ in range(1000)]
print("done")
PY
python -m memray run -o poc.bin victim.py ''
python -m memray flamegraph -o poc.html poc.bin
Le HTML généré contenait du balisage brut contrôlé par l'attaquant :
Command line: <code>~/memray/src/memray/__main__.py run -o poc.bin victim.py </code><br>
Ouvrir ou actualiser le rapport généré déclenchait l'exécution de JavaScript.
Cela a établi l'affirmation centrale :
Plus tard, lors de la divulgation coordonnée, le mainteneur a simplifié davantage le reproducteur :
python -m memray run -o poc.bin -c '# '
Cette version est meilleure car elle isole la frontière vulnérable plus directement :
La charge utile était volontairement simple :
Il ne s'agit pas de charges utiles tape-à-l'œil.
C'est une sonde d'exécution propre car :
Pour cette classe de bug, cela suffit.
Un seul type de rapport aurait déjà suffi pour justifier le problème.
Mais je voulais savoir si cela était isolé ou structurel.
J'ai confirmé le même comportement dans :
--no-webCela comptait pour deux raisons.
Cela montrait que le sink vulnérable était réutilisé dans plusieurs sorties HTML.
Cela prouvait que le bug ne dépendait pas de ressources externes hébergées sur un CDN.
--no-web reproduisait toujours le problème, ce qui signifie que le problème venait du HTML généré par Memray lui-même et de la gestion des templates, et non du comportement du JS distant.
Cela a rendu l'argument beaucoup plus solide.
La principale préoccupation du mainteneur était le contrôle pratique de l'attaquant.
C'est justifié.
Ce n'est pas le genre de problème où un attaquant distant non authentifié touche un point de terminaison HTTP exposé et obtient un impact immédiat.
La condition d'exploitation est plus étroite :
La classification réaliste était donc une gravité faible.
Cela n'en fait pas un bug faible.
La gravité concerne les conditions d'exploitation et l'impact probable. La validité concerne la réalité du problème.
Ce problème était clairement réel :
C'est pourquoi il est quand même devenu une CVE.
Le correctif était minimal et correct.
Le mainteneur a modifié :
{{ metadata.command_line }}
en :
{{ metadata.command_line|e }}
C'est le bon correctif car il traite directement le sink vulnérable.
Au lieu d'émettre du balisage brut comme :
le template émet désormais du texte échappé :
<img src=x onerror=alert(1)>
Cela préserve la valeur informative du champ de ligne de commande tout en supprimant la voie d'exécution dans le navigateur.
Le mainteneur a également passé en revue le reste du contexte du template et a conclu que :
|tojson
Le problème a donc été correctement circonscrit à metadata.command_line.C'est exactement le type de revue de correctif que l'on attend dans une divulgation réelle.
Cela a été signalé en privé via les GitHub Security Advisories.
Les mainteneurs :
Le problème a reçu l'identifiant : CVE-2026-32722
La leçon clé ici est simple :
les métadonnées ne sont pas automatiquement dignes de confiance simplement parce qu'elles semblent opérationnelles.
Aucun de ces éléments n'a d'importance dès lors que du contenu contrôlé par un attaquant traverse la sortie rendue par le navigateur sans échappement.
Dès le moment où un outil émet du HTML, il doit être traité comme une application produisant du HTML.
C'est le véritable enseignement.
Cette vulnérabilité ne concernait pas une charge utile ingénieuse.
Il s'agissait d'identifier la bonne frontière.
Memray a pris des métadonnées de ligne de commande influencées par un attaquant et les a rendues dans du HTML généré sans les échapper. Le navigateur a fait le reste.
C'est pourquoi cela est devenu CVE-2026-32722.
Corrigé dans Memray 1.19.2.
