CVE-2026-84191
CVSS 6.1 · Moyenne CorrigéCVE-2026-84191 : XSS stockée dans LibreNMS via les noms et descriptions de VRF issus de SNMP
LibreNMS affichait sans échappement les noms, descriptions et route distinguishers de VRF lus en SNMP : un équipement hostile exécutait du script chez l’opérateur.
Pour décider
- Impact
- Des données SNMP malveillantes peuvent exécuter du JavaScript dans le navigateur d’un opérateur.
- Conditions
- Il faut contrôler un équipement supervisé ou pouvoir modifier le trafic SNMPv1/v2c, puis faire consulter les pages VRF concernées.
- Action recommandée
- Appliquer les corrections LibreNMS 26.5.0 et 26.8.0 décrites dans l’avis, ou une version ultérieure qui les inclut.
Fiche de l’avis
Synthèse de l’enquête
LibreNMS enregistrait le nom, la description et le route distinguisher des VRF renvoyés en SNMP par les équipements supervisés, puis les écrivait dans ses pages de routage sans échappement HTML. Un équipement contrôlé par l’attaquant, ou toute personne capable de modifier le trafic SNMPv1/v2c, pouvait y placer du JavaScript exécuté dans le navigateur de chaque utilisateur ouvrant ces pages. Points principaux corrigés en 26.5.0 ; un point onmouseover a été modifié en 26.8.0.
Le nom VRF venu du réseau
Nous avons traité les chaînes SNMP comme des entrées non fiables, même si elles arrivent par le réseau de supervision plutôt que par un formulaire. Dans LibreNMS, cleanPort() échappait déjà plusieurs champs de ports avant affichage. Nous avons cherché les autres valeurs recueillies par découverte qui n’avaient pas ce traitement, puis suivi les descriptions et noms VRF depuis les réponses SNMP jusqu’à la table vrfs et aux pages de routage. Plusieurs vues les écrivaient directement dans le HTML ; une infobulle les plaçait aussi dans un contexte JavaScript.
Nous avons documenté un payload de description VRF et la manière de contrôler directement le rendu d’une valeur stockée, afin de distinguer l’entrée SNMP du défaut d’affichage. Le signalement privé date du 24 mars 2026. LibreNMS a échappé les champs VRF et VLAN dans la version 26.5.0 de mai, puis réécrit l’infobulle pour 26.8.0 ; l’avis CVE-2026-84191 est paru entre ces deux étapes, en août. Ce décalage oblige à lire les versions du correctif par sink plutôt que d’annoncer une date unique où toutes les variantes auraient disparu.
Une base remplie par les équipements supervisés
LibreNMS stocke les chaînes renvoyées par les équipements pendant la découverte SNMP. Un nom ou une description de VRF reste donc une entrée distante, même lorsque la page de routage la relit ensuite dans la base locale.
La supervision inverse le sens habituel de la confiance. Dans une application web, la requête n’est pas fiable et la base de données appartient à l’application. Dans LibreNMS, une grande partie de la base est écrite par les équipements supervisés : noms d’interfaces, descriptions, contacts système, tables de routage. Chacune de ces chaînes provient d’un matériel qui peut se trouver chez un client, sur le réseau d’un partenaire ou dans un laboratoire, tourner avec un firmware ancien, voire être déjà compromis. C’est une entrée venue de l’extérieur, livrée par SNMP au lieu de HTTP.
De la valeur SNMP à la vue de routage
cleanPort() fait déjà passer ifAlias, ifName et ifDescr par htmlentities() avant l’affichage des ports. Nous en avons fait un contrôle : quelles autres chaînes collectées atteignaient une page sans encodage équivalent ?
Restait à trouver les champs collectés qui ne passent par aucune fonction de ce genre. Suivre les données VRF de la découverte jusqu’à l’affichage a donné un chemin direct. includes/discovery/vrf.inc.php parcourt MPLS-L3VPN-STD-MIB, MPLS-VPN-MIB ou CISCO-VRF-MIB selon ce que l’équipement répond, et enregistre le résultat dans la table vrfs avec dbInsert() et dbUpdate(). Les pages qui affichent les VRF écrivent ensuite ces colonnes telles quelles. L’analyse a porté sur le commit ff8cbea de mars 2026.
Cause racine
Trois colonnes sont en jeu, et l’équipement les contrôle toutes. Le route distinguisher est la valeur renvoyée pour mplsL3VpnVrfRD ou mplsVpnVrfRouteDistinguisher. La description vient de la colonne de description de la même MIB. Le nom de VRF est décodé à partir de l’index de l’OID lui-même, une suite de codes ASCII précédée de sa longueur :
// includes/discovery/vrf.inc.php
// 8.49.53.48.56.58.49.48.48 "1508:100"
// First digit gives number of chars in VRF Name, then it's ASCII
$oid_values = explode('.', $vrf_oid);
$vrf_name = '';
for ($i = 1; $i <= $oid_values[0] && isset($oid_values[$i]); $i++) {
$vrf_name .= chr($oid_values[$i]);
}
...
dbInsert(['vrf_oid' => $vrf_oid, 'vrf_name' => $vrf_name, 'mplsVpnVrfRouteDistinguisher' => $vrf_rd, 'mplsVpnVrfDescription' => $descr_table[$vrf_oid] ?? '', 'device_id' => $device['device_id']], 'vrfs');
Un agent hostile peut donc choisir n’importe quel octet pour le nom, y compris <, > et les guillemets, simplement en choisissant l’index qu’il renvoie. Côté affichage, ces valeurs sont écrites dans le HTML sans échappement :
// includes/html/print-vrf.inc.php (commit ff8cbea)
echo "<td width=200 class=list-large><a href='routing/vrf/" . $vrf['mplsVpnVrfRouteDistinguisher'] . "/'>" . $vrf['vrf_name'] . '</a></td>';
echo '<td width=150 class=box-desc>' . $vrf['mplsVpnVrfDescription'] . '</td>';
echo '<td width=100 class=box-desc>' . $vrf['mplsVpnVrfRouteDistinguisher'] . '</td>';
// includes/html/pages/routing/vrf.inc.php, lignes 114 à 117 (commit ff8cbea)
echo $vrf['vrf_name'] . '</a><br />';
echo '<span class=box-desc>' . $vrf['mplsVpnVrfDescription'] . '</span></td>';
echo '<td width=100 class=box-desc>' . $vrf['mplsVpnVrfRouteDistinguisher'] . '</td>';
La même page place aussi vrf_name dans une chaîne JavaScript à l’intérieur d’un attribut onmouseover, affiché lorsque le nom configuré sur un équipement diffère du nom attendu. Dans ce contexte, une apostrophe suffit pour sortir de la chaîne, sans aucune balise :
// includes/html/pages/routing/vrf.inc.php, ligne 140 (commit ff8cbea)
echo "<a href='#' onmouseover=\" return overlib('Expected Name : " . $vrf['vrf_name'] . '<br />Configured : ' . $device['vrf_name'] . "', ...
Le rapport signalait aussi le même motif pour les noms de VLAN dans includes/html/pages/device/port/vlans.inc.php. Il s’agit de CWE-79, cross-site scripting stocké : le payload est écrit une fois en base et servi à chaque visiteur.
Exploitation
L’attaquant a besoin d’un équipement supervisé par LibreNMS qui réponde aux requêtes VRF avec des valeurs choisies. Un équipement de test, un simulateur SNMP ou un routeur compromis conviennent. Le payload de l’avis utilise la description :
MPLS-VPN-MIB::mplsVpnVrfDescription.XSS_TEST = STRING: <img src=x onerror=alert(document.cookie)>
Au passage de découverte suivant, LibreNMS enregistre la chaîne dans vrfs.mplsVpnVrfDescription. Lorsqu’un utilisateur ouvre Routing, puis VRFs, ou une autre page qui liste les VRF de l’équipement, la page contient l’élément img et le navigateur exécute le gestionnaire onerror dans l’origine de LibreNMS. Pour une démonstration rapide sans SNMP, l’avis insère la même valeur directement dans la table :
INSERT INTO vrfs (device_id, vrf_oid, vrf_name, mplsVpnVrfRouteDistinguisher, mplsVpnVrfDescription)
VALUES (1, '1', 'test_vrf', '65000:1', '<img src=x onerror=alert(document.cookie)>');
Posséder un équipement n’est pas la seule voie. SNMPv1 et SNMPv2c fonctionnent sur UDP, transmettent la communauté en clair et n’offrent aucune intégrité des messages. Un attaquant placé entre le collecteur et n’importe quel équipement, sur un segment d’administration partagé ou un lien opérateur, peut réécrire la réponse à une requête GetBulk ou GetNext et injecter le payload au nom d’un équipement qui n’a jamais été compromis. SNMPv3 en mode authPriv ferme cette voie, car les réponses sont authentifiées et chiffrées.
Impact
Le vecteur est AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N, pour un score de 6,1. L’attaquant n’a besoin d’aucun compte LibreNMS, un utilisateur connecté doit ouvrir une page concernée, et le script s’exécute dans la session de cet utilisateur, hors de l’équipement qui l’a déposé, d’où le changement de portée.
L’avis cite le vol de session et la prise de contrôle de comptes. Une nuance s’applique : Laravel émet par défaut son cookie de session avec le drapeau HttpOnly, si bien que lire document.cookie, comme dans la preuve de concept, peut ne pas renvoyer la session elle-même. Cela change la technique plus que le résultat. Le script s’exécute dans l’origine authentifiée de LibreNMS et peut envoyer des requêtes de même origine avec les droits de l’utilisateur : lire l’inventaire et les pages de configuration, modifier des règles d’alerte ou, si le visiteur est administrateur, créer un compte. Le payload reste en base et se déclenche à nouveau pour chaque utilisateur qui ouvre la page, aussi longtemps que l’équipement continue de le renvoyer.
Correctif
Les principaux points d’injection ont été corrigés dans la PR #19660 (« Various XSS fixes »), fusionnée sous le commit 6782af9 et livrée en 26.5.0. La sortie est échappée avec la fonction e() de Laravel, qui enveloppe htmlspecialchars() avec ENT_QUOTES :
// includes/html/print-vrf.inc.php (26.5.0)
echo "<td width=200 class=list-large><a href='routing/vrf/" . e($vrf['mplsVpnVrfRouteDistinguisher']) . "/'>" . e($vrf['vrf_name']) . '</a></td>';
echo '<td width=150 class=box-desc>' . e($vrf['mplsVpnVrfDescription']) . '</td>';
echo '<td width=100 class=box-desc>' . e($vrf['mplsVpnVrfRouteDistinguisher']) . '</td>';
Le même commit échappe les trois champs dans pages/routing/vrf.inc.php, le nom, le coût, la priorité et l’état des VLAN dans vlans.inc.php, ainsi que des sorties dans d’autres pages historiques (BGP, capteurs, stockage, processus, syslog, journal d’événements). Échapper à la sortie, là où le contexte HTML est connu, est la bonne couche pour des données qui viennent des équipements.
Deux points méritent l’attention. D’abord, le point d’injection onmouseover n’a pas été modifié par ce commit. Il a été réécrit plus tard, dans le commit 38ac874 (« Replace graph.php links »), qui échappe les deux noms avec e() et construit l’infobulle via Url::overlibLink() ; ce changement apparaît pour la première fois en 26.8.0. Ensuite, la plage affectée de l’avis, ≤ 25.3.0, ne correspond pas à sa propre version corrigée : le code vulnérable a été examiné sur un commit de mars 2026, postérieur à la 26.3.1, et le correctif n’est arrivé qu’en 26.5.0. La fiche CVE décrit les versions antérieures à 26.5.0 comme affectées, et celles antérieures à 26.8.0 pour la variante onmouseover.
Détection et mitigation
- Mettre à jour LibreNMS vers la 26.8.0 ou une version ultérieure, qui couvre à la fois les points d’injection de l’avis et la variante
onmouseover; la 26.5.0 est le minimum pour les points cités dans l’avis. - Passer la collecte en SNMPv3
authPrivpartout où les équipements le permettent, pour que les réponses ne puissent pas être modifiées en transit, et cantonner SNMPv1/v2c à un réseau d’administration dédié. - Rechercher du balisage dans les données VRF en base :
SELECT device_id, vrf_name, mplsVpnVrfDescription, mplsVpnVrfRouteDistinguisher FROM vrfs WHERE CONCAT_WS(' ', vrf_name, mplsVpnVrfDescription, mplsVpnVrfRouteDistinguisher) REGEXP '[<>\'"]';. Des noms de VRF et des route distinguishers légitimes ne contiennent pas ces caractères. - Appliquer le même examen aux noms de VLAN et aux autres champs SNMP en texte libre affichés dans l’interface, et retirer de la supervision, ou isoler, les équipements dont les réponses contiennent du balisage.
- Limiter les comptes administrateurs aux personnes qui en ont besoin et éviter de consulter les pages de routage avec une session administrateur sur une version non corrigée, puisque tout script exécuté hérite des droits du visiteur.
Chronologie
| Date | Événement |
|---|---|
| 24/03/2026 | Signalement privé aux mainteneurs via un avis de sécurité GitHub |
| 14/05/2026 | Fusion de la PR #19660 (6782af9), qui échappe les champs VRF et VLAN |
| 18/05/2026 | Publication de LibreNMS 26.5.0 |
| 24/07/2026 | Réécriture de l’infobulle onmouseover avec des noms échappés (38ac874) |
| 04/08/2026 | Publication de l’avis GHSA-g993-wffj-m3gv |
| 17/08/2026 | Publication de LibreNMS 26.8.0, première version avec la nouvelle infobulle |
Références
- Avis de l’éditeur · CVE-2026-84191 github.com/librenms/librenms/security/advisories/GHSA-g993-wffj-m3gv
- Fiche NVD · CVE-2026-84191 nvd.nist.gov/vuln/detail/CVE-2026-84191
- Fiche CVE.org · CVE-2026-84191 www.cve.org/CVERecord?id=CVE-2026-84191
- MITRE CWE-79 cwe.mitre.org/data/definitions/79.html
- Dépôt du projet github.com/librenms/librenms
Citer cet avis
rootzero. « CVE-2026-84191 : XSS stockée dans LibreNMS via les noms et descriptions de VRF issus de SNMP ». Recherche de vulnérabilités, rootzero, 4 août 2026. https://rootzero.eu/research/cve/cve-2026-84191-librenms-stored-xss-snmp/
#Recherche de vulnérabilités
Faites auditer votre code avant qu’un autre ne le fasse.
Votre équipe doit décider quoi corriger avant une livraison ou évaluer une dépendance critique. Nous définissons une mission autour de cette décision, avec des preuves et des priorités de correction expliquées.