GHSA-w4g9-mxgg-j532 Nezha Monitoring: RoleMember-reachable SSRF with full response-body reflection via POST /api/v1/notification
Summarynezha'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 versionsCommit `50dc8e660326b9f22990898142c58b7a5312b42a` and earlier on `master`.Reachability chainFor comparison, `/user` routes ARE gated by `adminHandler`:`adminHandler` (controller.go:220-236) explicitly enforces `user.Role.IsAdmin()`. `commonHandler` (controller.go:214-218) does not.The vulnerable handlerIdentical pattern in `updateNotification` (PATCH /notification/:id) at lines 97-146.The reflection sinkThe 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.PoCA. Read intranet admin-panel response bodyResponse:B. AWS IMDSv2 reachability + body leakIMDSv2 returns 401 with a body explaining the missing token; that body is reflected.C. DoS via large internal fileBecause 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**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.**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.**Cap response body**: `io.LimitReader(resp.Body, 4096)`. 4 KB is plenty for surfacing webhook errors.**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 environmentTested 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)ReporterEddie 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.
CVSS v3.1 score: 8.5