VYPR
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.

PackageAffected versionsPatched versions
uptime-kumanpm
>= 2.0.0, < 2.2.02.2.0

Affected products

1

Patches

1
303a609c05d0

Merge commit from fork

https://github.com/louislam/uptime-kumaLouis LamMar 5, 2026via ghsa
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

News mentions

0

No linked articles in our index yet.