Skip to main content

nezha CVE-2026-49397

MEDIUM
Information Exposure (CWE-200)
2026-06-10 https://github.com/nezhahq/nezha GHSA-vrmh-5mmx-hjwx
5.3
CVSS 3.1 · Vendor: https://github.com/nezhahq/nezha
View

Attack ChainAIDerived

Hypothetical attack flow derived from CVE metadata – not a confirmed exploit chain

  1. Access
    Identify public server IDs via numeric iteration
  2. Delivery
    Call GET /api/v1/server/{id}/service unauthenticated
  3. Exploit
    Receive hidden service names, IDs, and timing data in response
  4. Execution
    Confirm leaked IDs via GET /api/v1/service/{id}/history
  5. Impact
    Map full private service topology from response data

Vulnerability Assessment

Weakness
CWE
OWASP Top 10:2025
A01:2025 - Broken Access Control
ASVS 5.0
V14 - Data Protection Data exposure prevention
MITRE ATT&CK

2 techniques across 2 tactics: Collection, Discovery

The technique list with ATT&CK links and the kill-chain view is part of the Basic plan and up.

Exploitation Conditions

Exploitation requires no authentication - the CVSS vector PR:N confirms unauthenticated access, and both vulnerable endpoints are mounted on nezha's optionalAuth middleware group, which permits unauthenticated callers. No special configuration is needed on the attacker side. On the victim side, exploitation produces meaningful information disclosure only when at least one service exists with EnableShowInService: false (the privacy feature must be in use). For the listServerServices path, the hidden service must monitor at least one server that is publicly visible (HideForGuest: false), giving the attacker a valid server ID as an enumeration entry point. For the getServiceHistory path, the attacker only needs to guess or iterate numeric service IDs, which is the looser constraint. Exploitation is limited to confidentiality (service names, IDs, timing patterns) - there is no write or execution primitive in this path. Deployments where all services are public or no services are marked private are unaffected in practice.

Impact

The CVSS 3.1 vector AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N yields 5.3 Medium, which accurately reflects the network-accessible, zero-authentication, low-complexity exploitation path and the bounded confidentiality impact (service names and timing data, not credentials or arbitrary data). No EPSS score was provided in the input data, so exploitation probability cannot be quantified from that signal. This vulnerability is not listed in the CISA KEV catalog, indicating no confirmed widespread active exploitation at time of analysis. However, a detailed proof-of-concept with working curl commands is documented in the public GitHub advisory, meaning any attacker who reads the advisory can exploit it immediately - the gap between 'no KEV' and 'trivially exploitable with public POC' is narrow. Real-world risk is elevated for multi-tenant or public-facing nezha deployments where the EnableShowInService flag is actively used to partition a private monitoring tier from a public dashboard; in those deployments the confidentiality loss is meaningful (service names routinely encode business asset topology). Deployments where all services are already public or where no services are marked private are unaffected in practice.

Exploit ScenarioDerived

An unauthenticated external visitor iterates small numeric server IDs (e.g., 1 through 200) against a public nezha deployment by calling GET /api/v1/server/{id}/service for each. For any server ID that is publicly visible (HideForGuest: false), the response includes the full list of services monitoring that server - including those marked EnableShowInService: false - along with their names, IDs, and per-server timing data. The attacker then confirms each leaked service ID via GET /api/v1/service/{id}/history to recover service names even when no public server exists as an enumeration anchor. A working curl-based proof-of-concept demonstrating this full flow is published in the GitHub security advisory.

This Month Plan within 30 days. Why: MEDIUM severity (CVSS 4.0-6.9).
Patch available 0.0% exploitation probability (EPSS)

Get an e-mail when this CVE is exploited, patched or re-scored. Free account.

Alert me

AnalysisAIv1 – Jun 10, 2026

Information disclosure in nezha 2.x server monitoring dashboard exposes private services marked with EnableShowInService: false to unauthenticated network visitors through two API endpoints that bypass the intended visibility filter. Attackers who can reach the API can enumerate hidden service names, IDs, and per-server timing data by iterating over public server IDs or guessing numeric service IDs - both low-cardinality spaces in typical deployments. No public exploit is required to leverage this; a fully functional proof-of-concept using only standard curl commands is documented in the GitHub security advisory GHSA-vrmh-5mmx-hjwx, and the fix is available in version 2.0.14.

RemediationAI

Upgrade nezha to version 2.0.14 or later, which is the vendor-released patch confirmed by the GitHub advisory and package metadata. The fix closes the inconsistent EnableShowInService enforcement gap across the listServerServices and getServiceHistory endpoints. Full advisory details are at https://github.com/nezhahq/nezha/security/advisories/GHSA-vrmh-5mmx-hjwx. If immediate upgrade is not possible, a compensating control is to place the nezha API behind a reverse proxy with authentication enforcement on the two affected routes (/api/v1/server/:id/service and /api/v1/service/:id/history), preventing unauthenticated access; this trade-off is that it breaks legitimate unauthenticated use of those endpoints for public status pages and must be reversed once patched. There is no configuration-only mitigation within nezha itself: setting all services to EnableShowInService: true eliminates the disclosure but defeats the privacy feature entirely.

Affected Packages

go github.com/nezhahq/nezha fix: 2.0.14

Source: OSV (Open Source Vulnerabilities)

Affects your stack

Affects 1 package in go

Checking this CVE against your own stacks and servers is part of the Basic plan and up.

Technical ContextAI · show details

Nezha is a Go-based open-source server monitoring and public status-page platform (pkg:go/github.com/nezhahq/nezha). Its internal ServiceSentinel singleton manages service state and exposes several accessor methods: CopyStats(), Get(id), and GetSortedList(). The EnableShowInService visibility flag is only enforced in CopyStats() (servicesentinel.go:421-438), which is the path behind the main public listing endpoint. However, Get() and GetSortedList() return raw, unfiltered service objects. Two routes in the optionalAuth middleware group - GET /api/v1/server/:id/service (listServerServices, service.go:258-340) and GET /api/v1/service/:id/history (getServiceHistory, service.go:126-180) - consume these unfiltered accessors and emit service identity data (name, ID, timing) in their responses without re-checking EnableShowInService. The root cause class is CWE-200 (Exposure of Sensitive Information), with the deeper structural issue being CWE-285/CWE-863: the authorization gate is implemented as a single point in one accessor rather than enforced consistently across all data-reader paths that emit sensitive service identity to external callers.

Know when Hashicorp gets its next vulnerability

  • Watch products and get an email when something new is published
  • Check this CVE against your own stack and servers
  • Exploit and PoC references, threat intelligence from 22 sources

No credit card · 7-day full trial

Severity by source

Vendor (https://github.com/nezhahq/nezha) PRIMARY 5.3 MEDIUM
AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N
SUSE MEDIUM

Primary rating from Vendor (https://github.com/nezhahq/nezha).

CVSS VectorVendor: https://github.com/nezhahq/nezha

Attack Vector
Network
Attack Complexity
Low
Privileges Required
None
User Interaction
None
Scope
Unchanged
Confidentiality
Low
Integrity
None
Availability
None

DescriptionCVE.org

Private services (EnableShowInService: false) are enumerable via per-server endpoints, leaking name and timing data

CWE: CWE-285 (Improper Authorization) via CWE-200 (Exposure of Sensitive Information to an Unauthorized Actor) and CWE-863 (Incorrect Authorization - inconsistent gating across data-reader paths)

CVSS v3.1: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N → 5.3 (Medium)

Summary

The EnableShowInService flag on a Service is meant to gate that service's visibility from the public dashboard. The main service-listing endpoint (GET /api/v1/service → showService) correctly filters services with EnableShowInService: false via ServiceSentinel.CopyStats() (service/singleton/servicesentinel.go:421-438). However, two adjacent reader endpoints retrieve service objects through code paths that do not honor the same flag:

  • GET /api/v1/server/:id/service (listServerServices) iterates ServiceSentinel.GetSortedList() (which returns every service regardless of visibility) and emits service ID, name, and timing data for any service monitoring the queried server.
  • GET /api/v1/service/:id/history (getServiceHistory) calls ServiceSentinel.Get(serviceID) directly and emits the service name (and aggregated per-server stats for servers the viewer can see).

Both endpoints are mounted on the optionalAuth group, so an unauthenticated visitor can enumerate hidden services as long as they can guess a public server ID (linear scan over a small numeric ID space) or a service ID (likewise). The service owner's intent - "hide this from the public" via EnableShowInService: false - is silently bypassed.

Affected

  • nezha master at HEAD 636f4a99e6c3d8d75f17fdf7ad55d4ee0f73f1c0 (the audit checkout)
  • All recent 2.x releases that share this code path (post the EnableShowInService filter introduction at CopyStats)

Vulnerability details

[A] - single-source-of-truth filter exists at the listing site

service/singleton/servicesentinel.go:421-438:

go
func (ss *ServiceSentinel) CopyStats() map[uint64]model.ServiceResponseItem {
    var stats map[uint64]*serviceResponseItem
    copier.Copy(&stats, ss.LoadStats())

    sri := make(map[uint64]model.ServiceResponseItem)
    for k, service := range stats {
        if !service.service.EnableShowInService {       // [A] filter here
            delete(stats, k)
            continue
        }
        service.ServiceName = service.service.Name
        sri[k] = service.ServiceResponseItem
    }
    return sri
}

CopyStats() is the only reader that respects EnableShowInService. Get() and GetSortedList() immediately below it return the raw services with no such filter:

go
func (ss *ServiceSentinel) Get(id uint64) (s *model.Service, ok bool) {
    ss.servicesLock.RLock(); defer ss.servicesLock.RUnlock()
    s, ok = ss.services[id]
    return                                              // [A'] no EnableShowInService check
}

[B] - listServerServices iterates GetSortedList() and emits hidden services

cmd/dashboard/controller/service.go:258-340 (GET /api/v1/server/:id/service):

go
func listServerServices(c *gin.Context) ([]*model.ServiceInfos, error) {
    // ... server existence + userCanViewServer check ...
    services := singleton.ServiceSentinelShared.GetSortedList()      // [B] all services, no filter

    for _, service := range services {
        if service.Cover == model.ServiceCoverAll {
            if service.SkipServers[serverID] { continue }
        } else {
            if !service.SkipServers[serverID] { continue }
        }
        // ... fetch history ...
        infos := &model.ServiceInfos{
            ServiceID:   service.ID,
            ServerID:    serverID,
            ServiceName: service.Name,                  // [B'] leaked
            ServerName:  server.Name,
            // ... timing data ...
        }
        result = append(result, infos)
    }
    return result, nil
}

The DB-fallback path at queryServerServicesFromDB (service.go:340-) has the same structure: iterates services (the same GetSortedList() output) and emits ServiceName for any service monitoring serverID.

[C] - getServiceHistory returns the service name for any ID

cmd/dashboard/controller/service.go:126-180 (GET /api/v1/service/:id/history):

go
func getServiceHistory(c *gin.Context) (*model.ServiceHistoryResponse, error) {
    serviceID, _ := strconv.ParseUint(c.Param("id"), 10, 64)
    service, ok := singleton.ServiceSentinelShared.Get(serviceID)   // [C] no filter
    if !ok || service == nil {
        return nil, singleton.Localizer.ErrorT("service not found")
    }
    // period restriction for guests (1d only) - but the service exists,
    // and ServiceName is set unconditionally:
    response := &model.ServiceHistoryResponse{
        ServiceID:   serviceID,
        ServiceName: service.Name,                       // [C'] leaked
        Servers:     make([]model.ServerServiceStats, 0),
    }
    // ... per-server data is filtered via userCanViewServer - that part is correct ...
    return response, nil
}

The per-server data inside the response IS correctly filtered via userCanViewServer. The service NAME is not.

The mismatch

[A] (CopyStats) gates by EnableShowInService because that's the listing endpoint's contract. [A'] (Get) / GetSortedList() return the raw data because they're "internal" accessors. But [B] and [C] are public-reachable endpoints that use those raw accessors and emit identifying information about services the owner marked as private. The visibility flag exists; it just isn't enforced at every reader of the same data.

A correct guard would either:

  • Move the EnableShowInService filter into Get() / GetSortedList() themselves, gated by "caller is admin or service owner"
  • Re-check EnableShowInService at every endpoint that emits service identity (name/id/timing)

Proof of concept

Setup (any nezha 2.x deployment):

  1. User A (member) creates a Service "Internal-CRM-Health" with EnableShowInService: false, monitoring server S which is public (HideForGuest: false).
  2. The service does not appear in GET /api/v1/service (the main listing correctly hides it).

Enumeration as an unauthenticated guest:

bash
# Find services that monitor server S
curl -s 'https://nezha.example/api/v1/server/'"$S_ID"'/service'
# →
# {"success":true,"data":[
#   {"service_id":42,"server_id":1,"service_name":"Internal-CRM-Health","server_name":"web-01",
#    "display_index":0,"created_at":[...],"avg_delay":[...]}
# ]}
#
# Hidden service is leaked: ID, name, and per-server timing data are all visible.

Confirmation via the second endpoint:

bash
curl -s 'https://nezha.example/api/v1/service/42/history?period=1d'
# →
# {"success":true,"data":{
#   "service_id":42,
#   "service_name":"Internal-CRM-Health",  ← leaked even for direct ID lookup
#   "servers":[]                            ← per-server data correctly hidden
# }}

A scripted enumeration over public server IDs (a low-cardinality numeric space - typical nezha deployments have <1000 servers) trivially recovers the full set of hidden services that monitor any public server, along with their names and timing patterns.

Impact

Direct

Service names in nezha deployments are frequently descriptive of the underlying business asset they monitor: "Production CRM Monitor", "Internal Wiki Health", "Backup-Vault Connectivity", "Stripe Webhook Latency". The leak therefore:

  • Discloses the existence and purpose of internal services that the owner explicitly hid from the public dashboard.
  • Exposes timing/latency data for the monitored relationship between a private service and any public server it touches - sufficient for a competitor or attacker to infer business activity patterns, outage windows, and probable backend topology.
  • Confirms presence/absence of a service ID via the second endpoint - an oracle that lets an unauthenticated visitor enumerate the service-id namespace and learn the deployment's service count and naming convention even when no public servers exist as enumeration vectors.

Indirect / second-order

  • Affects multi-tenant public dashboards: nezha is frequently deployed as a public status page with a private "internal" tier in the same dashboard. The bypass collapses the privacy boundary between these tiers.
  • Composability with prior advisories: the recent fixes for GHSA-rxf6-wjh4-jfj6 (cross-user trigger-task firing), GHSA-hvv7-hfrh-7gxj (WS server-stream cross-tenant leak), and GHSA-4g6j-g789-rghm (forged monitor results) all address the cross-tenant visibility model. This finding is a sibling that closes one more reader gap in the same model.

Suggested fix

Either of:

  1. Centralize the filter in ServiceSentinel - change Get(id) and GetSortedList() to accept the *gin.Context (or a viewer context) and apply the EnableShowInService filter plus an admin-or-owner override. This guarantees every reader inherits the gate:
go
   func (ss *ServiceSentinel) GetForViewer(c *gin.Context, id uint64) (*model.Service, bool) {
       s, ok := ss.Get(id)
       if !ok { return nil, false }
       if !s.EnableShowInService && !callerIsAdminOrOwns(c, s) {
           return nil, false
       }
       return s, true
   }
  1. Recheck at every endpoint that emits service identity - add the EnableShowInService + ownership check at the top of listServerServices, getServiceHistory, and anywhere else GetSortedList()/Get() results flow to a response. More surgical but easier to miss next time.

Option (1) is symmetric with how userCanViewServer centralizes the server-visibility decision; the same pattern at the service layer would close this class once.

Threat Intelligence Overview

EPSS Score
0.0%
Top 91% exploitable
Threat Intel
1
1 source
Social Media
1
Sightings
Data Quality
5 sources
Confidence: Medium
NVD CVE.org EPSS Vulnrichment GHSA Vendor CIRCL GitHub

How this decision was reached

The engine reads each signal below and applies a fixed list of rules from top to bottom. The first rule that matches sets the recommended action – like a statement, every line is shown.

SignalValue
Severity MEDIUM
CISA KEV No
Public POC No
EPSS 0.0%
Patch available Yes
Active threat No
Deciding rule
MEDIUM severity (CVSS 4.0-6.9)
What every action level means
Emergency Immediate action required. Criteria: CRITICAL + in CISA KEV (actively exploited) without patch, OR CRITICAL + EPSS > 30% without patch, OR CRITICAL + active threat signals without patch.
Act Now Prioritize within 24-48h. Criteria: in KEV with patch available, OR CRITICAL + public POC, OR HIGH + EPSS > 10%, OR CRITICAL + active threat, OR any CRITICAL severity.
This Week Schedule remediation this week. Criteria: HIGH severity (CVSS 7.0-8.9). Patch within normal change window.
This Month Plan within 30 days. Criteria: MEDIUM severity (CVSS 4.0-6.9), lower exploitation probability. Include in regular patch cycle.
Monitor Track and reassess. Criteria: LOW severity (CVSS 0.1-3.9), minimal exploitation risk. No immediate action needed.

SSVC AssessmentENISA

SSVC Decision· derived
Scheduled
Exploitation
Poc
System Exposure· derived
Open
Automatable
Yes
Human Impact· derived
Medium
Technical Impact
Partial

Priority Score

26
Low Medium High Critical
KEV: 0
EPSS: 0
CVSS: +26
PoC: 0
Weaponized: 0
Threat intel: 0
Gov advisory: 0

Attack Technique

Information disclosure occurs when an application unintentionally exposes sensitive data that aids attackers in reconnaissance or directly compromises security.

Affected Products (CPE)CVE.org

pkg:go/github.com_nezhahq_nezha
CVE-2026-45321 CRITICAL POC
9.6 May 12

Credential-harvesting malware compromised 84 versions of 42 TanStack npm packages on 2026-05-11 via chained GitHub Actio

CVE-2026-34976 CRITICAL POC
10.0 Apr 02

Unauthenticated remote attackers can trigger complete database overwrites, server-side file reads, and SSRF attacks agai

CVE-2021-30476 CRITICAL POC
9.8 Apr 22

HashiCorp Terraform’s Vault Provider (terraform-provider-vault) did not correctly configure GCE-type bound labels for Va

CVE-2019-7442 CRITICAL POC
9.8 May 08

An XML external entity (XXE) vulnerability in the Password Vault Web Access (PVWA) of CyberArk Enterprise Password Vault

CVE-2018-20371 CRITICAL POC
9.8 Dec 23

PhotoRange Photo Vault 1.2 appends the password to the URI for authorization, which makes it easier for remote attackers

CVE-2018-9843 CRITICAL POC
9.8 Apr 12

The REST API in CyberArk Password Vault Web Access before 9.9.5 and 10.x before 10.1 allows remote attackers to execute

CVE-2026-54725 CRITICAL POC
9.6 Jul 31

Server-side request forgery in bank-vaults vault-secrets-webhook (<= 1.22.2) lets a low-privileged tenant with ConfigMap

CVE-2026-60104 CRITICAL POC
9.3 Jul 08

Account takeover in self-hosted Bitwarden Server before 2026.6.0 lets a low-privileged organization member steal any oth

CVE-2021-43837 CRITICAL POC
9.1 Dec 16

vault-cli is a configurable command-line interface tool (and python library) to interact with Hashicorp Vault. Rated cri

CVE-2020-16272 CRITICAL POC
9.1 Aug 03

The SRP-6a implementation in Kee Vault KeePassRPC before 1.12.0 is missing validation for a client-provided parameter, w

CVE-2020-16271 CRITICAL POC
9.1 Aug 03

The SRP-6a implementation in Kee Vault KeePassRPC before 1.12.0 generates insufficiently random numbers, which allows re

CVE-2026-53727 HIGH POC
8.9 Jul 09

Server-side request forgery in the Ruby css_parser gem (< 3.0.0) lets a remote unauthenticated attacker force the parsin

Vendor StatusVendor

SUSE

Severity: Moderate
Product Status
SUSE Linux Enterprise Server 16.1 Affected
SUSE Linux Enterprise Server for SAP applications 16.1 Affected
SUSE Linux Enterprise Module for Package Hub 15 SP5 Affected
SUSE Linux Enterprise Module for Package Hub 15 SP6 Affected
openSUSE Leap 15.5 Affected

This site uses cookies essential for authentication and security. No tracking or analytics cookies are used. Privacy Policy