nezha CVE-2026-49397
MEDIUMAttack ChainAIDerived
Hypothetical attack flow derived from CVE metadata – not a confirmed exploit chain
-
AccessIdentify public server IDs via numeric iteration
-
DeliveryCall GET /api/v1/server/{id}/service unauthenticated
-
ExploitReceive hidden service names, IDs, and timing data in response
-
ExecutionConfirm leaked IDs via GET /api/v1/service/{id}/history
-
ImpactMap 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 |
| 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 |
| Exploit ScenarioDerived | An unauthenticated external visitor iterates small numeric server IDs (e.g., 1 through 200) against a public nezha deployment by calling |
Get an e-mail when this CVE is exploited, patched or re-scored. Free account.
Alert meAnalysisAIv1 – 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
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
AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N
Primary rating from Vendor (https://github.com/nezhahq/nezha).
CVSS VectorVendor: https://github.com/nezhahq/nezha
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) iteratesServiceSentinel.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) callsServiceSentinel.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
masterat HEAD636f4a99e6c3d8d75f17fdf7ad55d4ee0f73f1c0(the audit checkout) - All recent 2.x releases that share this code path (post the
EnableShowInServicefilter introduction atCopyStats)
Vulnerability details
[A] - single-source-of-truth filter exists at the listing site
service/singleton/servicesentinel.go:421-438:
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:
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):
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):
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
EnableShowInServicefilter intoGet()/GetSortedList()themselves, gated by "caller is admin or service owner" - Re-check
EnableShowInServiceat every endpoint that emits service identity (name/id/timing)
Proof of concept
Setup (any nezha 2.x deployment):
- User A (member) creates a Service "Internal-CRM-Health" with
EnableShowInService: false, monitoring serverSwhich is public (HideForGuest: false). - The service does not appear in
GET /api/v1/service(the main listing correctly hides it).
Enumeration as an unauthenticated guest:
# 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:
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), andGHSA-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:
- Centralize the filter in
ServiceSentinel- changeGet(id)andGetSortedList()to accept the*gin.Context(or a viewer context) and apply theEnableShowInServicefilter plus an admin-or-owner override. This guarantees every reader inherits the gate:
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
}- Recheck at every endpoint that emits service identity - add the EnableShowInService + ownership check at the top of
listServerServices,getServiceHistory, and anywhere elseGetSortedList()/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
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.
What every action level means
SSVC AssessmentENISA
Priority Score
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
Credential-harvesting malware compromised 84 versions of 42 TanStack npm packages on 2026-05-11 via chained GitHub Actio
Unauthenticated remote attackers can trigger complete database overwrites, server-side file reads, and SSRF attacks agai
HashiCorp Terraform’s Vault Provider (terraform-provider-vault) did not correctly configure GCE-type bound labels for Va
An XML external entity (XXE) vulnerability in the Password Vault Web Access (PVWA) of CyberArk Enterprise Password Vault
PhotoRange Photo Vault 1.2 appends the password to the URI for authorization, which makes it easier for remote attackers
The REST API in CyberArk Password Vault Web Access before 9.9.5 and 10.x before 10.1 allows remote attackers to execute
Server-side request forgery in bank-vaults vault-secrets-webhook (<= 1.22.2) lets a low-privileged tenant with ConfigMap
Account takeover in self-hosted Bitwarden Server before 2026.6.0 lets a low-privileged organization member steal any oth
vault-cli is a configurable command-line interface tool (and python library) to interact with Hashicorp Vault. Rated cri
The SRP-6a implementation in Kee Vault KeePassRPC before 1.12.0 is missing validation for a client-provided parameter, w
The SRP-6a implementation in Kee Vault KeePassRPC before 1.12.0 generates insufficiently random numbers, which allows re
Server-side request forgery in the Ruby css_parser gem (< 3.0.0) lets a remote unauthenticated attacker force the parsin
Same weakness CWE-200 – Information Exposure
View allSame technique Information Disclosure
View allVendor 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 |
| openSUSE Leap 15.6 | Affected |
GHSA-vrmh-5mmx-hjwx
NVD
CVE.org
EPSS
Vulnrichment
Exploit-DB