CVE-2026-84191

CVSS 6.1 · Medium Patched
Type
Vulnerability research
Published
Reading time
7 min read
Author
rootzero
Lire en français
Lire en français →

CVE-2026-84191: stored XSS in LibreNMS through SNMP-sourced VRF names and descriptions

LibreNMS rendered VRF names, descriptions and route distinguishers polled over SNMP without escaping, so a rogue device could run script in an operator’s browser.

For your decision

Impact
Malicious SNMP data can execute JavaScript in an operator’s browser.
Conditions
The attacker must control a monitored device or modify SNMPv1/v2c traffic, and an operator must view the affected VRF pages.
Recommended action
Apply the LibreNMS 26.5.0 and 26.8.0 fixes described in the advisory, or a later version containing them.

Advisory facts

Product
LibreNMS
Package
composer · librenms/librenms
Affected versions
< 26.5.0 per CVE record; GHSA lists ≤ 25.3.0
Fixed in
26.5.0 (main sinks); 26.8.0 (onmouseover follow-up)
CVSS
6.1 Medium
Weakness
CWE-79
Access needed
No authentication
Advisory published
Credit
rootzero

Investigation summary

LibreNMS stored the VRF name, description and route distinguisher returned by monitored devices over SNMP, then echoed them into its routing pages without HTML escaping. A device under the attacker’s control, or anyone able to tamper with SNMPv1/v2c traffic, could plant JavaScript that runs in the browser of every user who opens those pages. Main rendering sinks fixed in 26.5.0; a separate onmouseover sink was changed in 26.8.0.

A VRF name from the network

We treated SNMP strings as untrusted input even though they arrive through monitoring rather than a web form. In LibreNMS, cleanPort() already escaped several port fields before display. We looked for other discovered values lacking that treatment, then followed VRF names and descriptions from SNMP responses into the vrfs table and routing pages. Several views wrote them straight into HTML; a tooltip also placed them in a JavaScript context.

We documented a VRF-description payload and a way to check the rendering of a stored value directly, separating the SNMP input from the display flaw. Our private report is dated 24 March 2026. LibreNMS escaped VRF and VLAN fields in May’s 26.5.0, then rewrote the tooltip for 26.8.0; CVE-2026-84191 appeared between those steps, in August. That gap means fixed versions need to be read per sink rather than described as one date when every variant disappeared.

A database filled by monitored devices

LibreNMS stores strings returned by network devices during SNMP discovery. A VRF name or description is therefore remote input even when a routing page later reads it from the local database.

Monitoring reverses the usual trust direction. In a web application, the request is untrusted and the database is the application’s own. In LibreNMS, much of the database is written by the monitored devices: interface names, descriptions, system contacts, routing tables. Every one of those strings comes from equipment that may sit in a customer site, a partner network or a lab, may run outdated firmware, and may already be compromised. It is input from outside, delivered over SNMP instead of HTTP.

From the SNMP value to the routing view

cleanPort() already passes ifAlias, ifName and ifDescr through htmlentities() before port display. We used that as a control: which other polled strings reached a page without equivalent encoding?

The question was then which polled fields do not go through such a function. Following VRF data from discovery to display gave a clean path. includes/discovery/vrf.inc.php walks MPLS-L3VPN-STD-MIB, MPLS-VPN-MIB or CISCO-VRF-MIB depending on what the device answers, and stores the result in the vrfs table with dbInsert() and dbUpdate(). The pages that display VRFs then echo those columns as they are. The analysis was done at commit ff8cbea of March 2026.

Root cause

Three columns are involved, and the device controls all of them. The route distinguisher is the value returned for mplsL3VpnVrfRD or mplsVpnVrfRouteDistinguisher. The description comes from the description column of the same MIB. The VRF name is decoded from the OID index itself, which is a length-prefixed sequence of ASCII codes:

// 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');

A rogue agent can therefore choose any byte for the name, including <, > and quotes, simply by choosing the index it returns. The rendering side writes these values into HTML without escaping:

// 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, lines 114 to 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>';

The same page also places vrf_name inside a JavaScript string in an onmouseover attribute, shown when the name configured on a device differs from the expected one. In that context a single quote is enough to leave the string, without any tag:

// includes/html/pages/routing/vrf.inc.php, line 140 (commit ff8cbea)
echo "<a href='#' onmouseover=\" return overlib('Expected Name : " . $vrf['vrf_name'] . '<br />Configured : ' . $device['vrf_name'] . "', ...

The report also pointed to the same pattern for VLAN names in includes/html/pages/device/port/vlans.inc.php. This is CWE-79, stored cross-site scripting: the payload is written once to the database and served to every viewer.

Exploitation

The attacker needs a device that LibreNMS polls and that answers VRF queries with chosen values. A test device, an SNMP simulator or a compromised router all work. The advisory’s payload uses the description:

MPLS-VPN-MIB::mplsVpnVrfDescription.XSS_TEST = STRING: <img src=x onerror=alert(document.cookie)>

At the next discovery run, LibreNMS stores the string in vrfs.mplsVpnVrfDescription. When a user opens Routing, then VRFs, or another page that lists the device’s VRFs, the page contains the img element and the browser runs the onerror handler in the LibreNMS origin. For a quick demonstration without SNMP, the advisory inserts the same value directly into the 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)>');

Owning a device is not the only way in. SNMPv1 and SNMPv2c run over UDP, send the community string in clear text and provide no message integrity. An attacker on the path between the poller and any device, on a shared management segment or a carrier link, can rewrite the response to a GetBulk or GetNext request and inject the payload on behalf of a device that was never compromised. SNMPv3 with authPriv closes that path, because responses are authenticated and encrypted.

Impact

The vector is AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N, for a score of 6.1. The attacker needs no LibreNMS account, a logged-in user has to open an affected page, and the script runs in that user’s session, outside the device that planted it, which is the scope change.

The advisory lists session theft and account takeover. One nuance applies: Laravel issues its session cookie with the HttpOnly flag by default, so reading document.cookie, as in the proof of concept, may not return the session itself. This changes the technique more than the outcome. The script runs in the authenticated LibreNMS origin and can send same-origin requests with the user’s rights: read the inventory and configuration pages, change alert rules, or, when the viewer is an administrator, create an account. The payload stays in the database and runs again for every user who opens the page, for as long as the device keeps returning it.

The fix

The main sinks were fixed in PR #19660 (“Various XSS fixes”), merged as commit 6782af9 and shipped in 26.5.0. Output is escaped with Laravel’s e() helper, which wraps htmlspecialchars() with 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>';

The same commit escapes the three fields in pages/routing/vrf.inc.php, the VLAN name, cost, priority and state in vlans.inc.php, and output in other legacy pages (BGP, sensors, storage, processes, syslog, event log). Escaping at output, where the HTML context is known, is the right layer for data that comes from devices.

Two points deserve attention. First, the onmouseover sink was left unchanged by that commit. It was rewritten later, in commit 38ac874 (“Replace graph.php links”), which escapes both names with e() and builds the tooltip through Url::overlibLink(); that change first shipped in 26.8.0. Second, the advisory’s affected range, ≤ 25.3.0, does not match its own patched version: the vulnerable code was reviewed at a March 2026 commit, after 26.3.1, and the fix only arrived in 26.5.0. Every release before 26.5.0 should be treated as affected, and releases before 26.8.0 for the onmouseover variant.

Detection and mitigation

  1. Upgrade LibreNMS to 26.8.0 or later, which covers both the sinks of the advisory and the onmouseover variant; 26.5.0 is the minimum for the sinks listed in the advisory.
  2. Move polling to SNMPv3 with authPriv wherever devices support it, so that responses cannot be modified in transit, and keep SNMPv1/v2c confined to a dedicated management network.
  3. Search the database for markup in VRF data: SELECT device_id, vrf_name, mplsVpnVrfDescription, mplsVpnVrfRouteDistinguisher FROM vrfs WHERE CONCAT_WS(' ', vrf_name, mplsVpnVrfDescription, mplsVpnVrfRouteDistinguisher) REGEXP '[<>\'"]';. Legitimate VRF names and route distinguishers do not contain these characters.
  4. Apply the same review to VLAN names and other free-text SNMP fields shown in the interface, and remove from monitoring, or isolate, devices whose answers contain markup.
  5. Limit administrator accounts to the people who need them and avoid browsing routing pages with an administrator session on unpatched versions, since any script that runs inherits the viewer’s rights.

Timeline

Date Event
2026-03-24 Private report to the maintainers through a GitHub security advisory
2026-05-14 PR #19660 merged (6782af9), escaping the VRF and VLAN fields
2026-05-18 LibreNMS 26.5.0 released
2026-07-24 onmouseover tooltip rewritten with escaped names (38ac874)
2026-08-04 Advisory GHSA-g993-wffj-m3gv published
2026-08-17 LibreNMS 26.8.0 released, first version with the tooltip change

References

  1. Vendor advisory · CVE-2026-84191 github.com/librenms/librenms/security/advisories/GHSA-g993-wffj-m3gv
  2. NVD entry · CVE-2026-84191 nvd.nist.gov/vuln/detail/CVE-2026-84191
  3. CVE.org record · CVE-2026-84191 www.cve.org/CVERecord?id=CVE-2026-84191
  4. MITRE CWE-79 cwe.mitre.org/data/definitions/79.html
  5. Project repository github.com/librenms/librenms

Cite this advisory

rootzero. “CVE-2026-84191: stored XSS in LibreNMS through SNMP-sourced VRF names and descriptions”. Vulnerability research, rootzero, 4 August 2026. https://rootzero.eu/en/research/cve/cve-2026-84191-librenms-stored-xss-snmp/

#Vulnerability research

Have your code audited before someone else does.

Your team needs to decide what to fix before a release or assess a critical dependency. We scope an engagement around that decision, with evidence and explained remediation priorities.