Moderate severityNVD Advisory· Published Mar 12, 2026· Updated Mar 13, 2026
Uptime Kuma is Missing Authorization Checks on Ping Badge Endpoint, Leaks Ping times of monitors without needing to be on a status page
CVE-2026-32230
Description
Uptime Kuma is an open source, self-hosted monitoring tool. From 2.0.0 to 2.1.3 , the GET /api/badge/:id/ping/:duration? endpoint in server/routers/api-router.js does not verify that the requested monitor belongs to a public group. All other badge endpoints check AND public = 1 in their SQL query before returning data. The ping endpoint skips this check entirely, allowing unauthenticated users to extract average ping/response time data for private monitors. This vulnerability is fixed in 2.2.0.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
uptime-kumanpm | >= 2.0.0, < 2.2.0 | 2.2.0 |
Affected products
1- Range: >= 2.0.0, < 2.2.0
Patches
1303a609c05d0Merge commit from fork
1 file changed · +27 −47
server/routers/api-router.js+27 −47 modified@@ -168,17 +168,7 @@ router.get("/api/badge/:id/status", cache("5 minutes"), async (request, response throw new Error("Invalid monitor ID"); } const overrideValue = value !== undefined ? parseInt(value) : undefined; - - let publicMonitor = await R.getRow( - ` - SELECT monitor_group.monitor_id FROM monitor_group, \`group\` - WHERE monitor_group.group_id = \`group\`.id - AND monitor_group.monitor_id = ? - AND public = 1 - `, - [requestedMonitorId] - ); - + const publicMonitor = await isMonitorPublic(requestedMonitorId); const badgeValues = { style }; if (!publicMonitor) { @@ -256,16 +246,7 @@ router.get("/api/badge/:id/uptime/:duration?", cache("5 minutes"), async (reques requestedDuration = `${requestedDuration}h`; } - let publicMonitor = await R.getRow( - ` - SELECT monitor_group.monitor_id FROM monitor_group, \`group\` - WHERE monitor_group.group_id = \`group\`.id - AND monitor_group.monitor_id = ? - AND public = 1 - `, - [requestedMonitorId] - ); - + const publicMonitor = await isMonitorPublic(requestedMonitorId); const badgeValues = { style }; if (!publicMonitor) { @@ -331,19 +312,20 @@ router.get("/api/badge/:id/ping/:duration?", cache("5 minutes"), async (request, } // Check if monitor is public + const publicMonitor = await isMonitorPublic(requestedMonitorId); const uptimeCalculator = await UptimeCalculator.getUptimeCalculator(requestedMonitorId); - const publicAvgPing = uptimeCalculator.getDataByDuration(requestedDuration).avgPing; + const avgPing = uptimeCalculator.getDataByDuration(requestedDuration).avgPing; const badgeValues = { style }; - if (!publicAvgPing) { + if (!publicMonitor) { // return a "N/A" badge in naColor (grey), if monitor is not public / not available / non exsitant badgeValues.message = "N/A"; badgeValues.color = badgeConstants.naColor; } else { - const avgPing = parseInt(overrideValue ?? publicAvgPing); + const avgPingValue = parseInt(overrideValue ?? avgPing); badgeValues.color = color; // use a given, custom labelColor or use the default badge label color (defined by badge-maker) @@ -353,7 +335,7 @@ router.get("/api/badge/:id/ping/:duration?", cache("5 minutes"), async (request, labelPrefix, label ?? `Avg. Ping (${requestedDuration.slice(0, -1)}${labelSuffix})`, ]); - badgeValues.message = filterAndJoin([prefix, avgPing, suffix]); + badgeValues.message = filterAndJoin([prefix, avgPingValue, suffix]); } // build the SVG based on given values @@ -467,17 +449,7 @@ router.get("/api/badge/:id/cert-exp", cache("5 minutes"), async (request, respon } const overrideValue = value && parseFloat(value); - - let publicMonitor = await R.getRow( - ` - SELECT monitor_group.monitor_id FROM monitor_group, \`group\` - WHERE monitor_group.group_id = \`group\`.id - AND monitor_group.monitor_id = ? - AND public = 1 - `, - [requestedMonitorId] - ); - + const publicMonitor = await isMonitorPublic(requestedMonitorId); const badgeValues = { style }; if (!publicMonitor) { @@ -554,17 +526,7 @@ router.get("/api/badge/:id/response", cache("5 minutes"), async (request, respon } const overrideValue = value && parseFloat(value); - - let publicMonitor = await R.getRow( - ` - SELECT monitor_group.monitor_id FROM monitor_group, \`group\` - WHERE monitor_group.group_id = \`group\`.id - AND monitor_group.monitor_id = ? - AND public = 1 - `, - [requestedMonitorId] - ); - + const publicMonitor = await isMonitorPublic(requestedMonitorId); const badgeValues = { style }; if (!publicMonitor) { @@ -656,4 +618,22 @@ function determineStatus(status, previousHeartbeat, maxretries, isUpsideDown, be } } +/** + * Check whether a monitor is publc + * @param {number} monitorID - Monitor id + * @returns {Promise<boolean>} true if the monitor is public, otherwise false + */ +async function isMonitorPublic(monitorID) { + let publicMonitor = await R.getRow( + ` + SELECT monitor_group.monitor_id FROM monitor_group, \`group\` + WHERE monitor_group.group_id = \`group\`.id + AND monitor_group.monitor_id = ? + AND public = 1 + `, + [monitorID] + ); + return !!publicMonitor; +} + module.exports = router;
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
7- github.com/advisories/GHSA-c7hf-c5p5-5g6hghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-32230ghsaADVISORY
- github.com/louislam/uptime-kuma/commit/303a609c05d0b174a5045c90f53c2b557d4febaeghsax_refsource_MISCWEB
- github.com/louislam/uptime-kuma/issues/7038ghsax_refsource_MISCWEB
- github.com/louislam/uptime-kuma/issues/7135ghsax_refsource_MISCWEB
- github.com/louislam/uptime-kuma/releases/tag/2.2.0ghsax_refsource_MISCWEB
- github.com/louislam/uptime-kuma/security/advisories/GHSA-c7hf-c5p5-5g6hghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.