Nezha Monitoring: RoleMember can fire other users' cron tasks via AlertRule.FailTriggerTasks (no ownership check)
Collected exploitation and prediction evidence
Risk source coverageCISA KEV is observed exploitation evidence. FIRST EPSS is a dated prediction of exploitation in the next 30 days. Missing scores are unknown. This collector requests predictions only for explicit CVE aliases in the public NuGet archive; Microsoft release views retain their existing prediction scope.
No collected risk observation for these identifiers. This does not establish absence of exploitation or a zero probability.
cve-program · CVE-2026-47120
PublishedNezha Monitoring is a self-hostable, lightweight, servers and websites monitoring and O&M tool. From version 1.4.0 to before version 2.0.8, a RoleMember can fire other users' cron tasks via AlertRule.FailTriggerTasks (no ownership check). This issue has been patched in version 2.0.8.
- Source published
- 12 Jun 2026 21:02:50 UTC
- Source modified
- 15 Jun 2026 17:27:46 UTC
- Observed here
- 23 Oct 2026 15:48:27 UTC
- Source terms
- CVE Program Terms of Use
Original source record · Cite this exact observation · Complete original publisher JSON
This view projects the publisher's source fields for reading; the original JSON retains additional fields and CVE enrichment containers. Attribution: cve-program · CVE Program Terms of Use. Source text is retained; field grouping and change labels are provided by ChangeIntel.
CVSS 3.1 · 7.1 CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:L
euvd · EUVD-2026-36593
PublishedNezha Monitoring is a self-hostable, lightweight, servers and websites monitoring and O&M tool. From version 1.4.0 to before version 2.0.8, a RoleMember can fire other users' cron tasks via AlertRule.FailTriggerTasks (no ownership check). This issue has been patched in version 2.0.8.
EUVD source date text has no stated timezone and is retained without inventing a UTC instant. Related bulletin membership is not an alias, common cause or applicability claim. A missing vector with a zero score is treated as unrated. Complete vendor/product and underlying source evidence remains in the original JSON.
Original publisher date fields
datePublished: Jun 12, 2026, 9:02:50 PM
dateUpdated: Jun 15, 2026, 5:27:46 PM
- Source published
- Jun 12, 2026, 9:02:50 PM (timezone not stated)
- Source modified
- Jun 15, 2026, 5:27:46 PM (timezone not stated)
- Observed here
- 7 Nov 2026 10:39:37 UTC
- Source terms
- ENISA EUVD · source terms and attribution apply
Original source record · Cite this exact observation · Complete original publisher JSON
This view projects the publisher's source fields for reading; the original JSON retains additional fields and CVE enrichment containers. Attribution: euvd · ENISA EUVD · source terms and attribution apply. Source text is retained; field grouping and change labels are provided by ChangeIntel.
EUVD CVSS 3.1 · 7.1 CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:L
nvd · CVE-2026-47120
PublishedNezha Monitoring is a self-hostable, lightweight, servers and websites monitoring and O&M tool. From version 1.4.0 to before version 2.0.8, a RoleMember can fire other users' cron tasks via AlertRule.FailTriggerTasks (no ownership check). This issue has been patched in version 2.0.8.
NVD analysis and CPE criteria are source context, not proof that a package or your environment is affected. Complete affected data, logical configurations and SSVC are retained in the original JSON.
Source analysis status: Deferred
Weakness classifications: CWE-862 · CWE-863
Original publisher date fields
published: 2026-06-12T22:16:51.100
lastModified: 2026-10-25T23:10:00.167
- Source published
- 12 Jun 2026 22:16:51 UTC
- Source modified
- 25 Oct 2026 23:10:00 UTC
- Observed here
- 27 Oct 2026 13:11:40 UTC
- Source terms
- NIST NVD usage terms; underlying source attribution retained
Original source record · Cite this exact observation · Complete original publisher JSON
This view projects the publisher's source fields for reading; the original JSON retains additional fields and CVE enrichment containers. Attribution: nvd · NIST NVD usage terms; underlying source attribution retained. Source text is retained; field grouping and change labels are provided by ChangeIntel.
CVSS 3.1 · [email protected] · Secondary · 7.1 CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:L
osv · GHSA-rxf6-wjh4-jfj6
Published## Summary `createAlertRule` and `createService` (and their `update*` siblings) accept `FailTriggerTasks []uint64` and `RecoverTriggerTasks []uint64` — IDs of cron tasks to fire when the alert/service trips. The validation function only validates the alert's `Rules.Ignore` server map; it never checks that the cron task IDs in `FailTriggerTasks` / `RecoverTriggerTasks` belong to the caller. When the alert fires, `singleton.CronShared.SendTriggerTasks(taskIDs, triggerServer)` (`service/singleton/crontask.go:113-127`) looks up those task IDs in the global cron registry and executes them via `CronTrigger`. For non-`AlertTrigger` cover modes, `CronTrigger` fans the command out to every server in `ServerShared.Range` with no ownership check. Net effect: a `RoleMember` can attach their alert rule (or service monitor) to **another user's** cron task ID — including admin's crons. When the alert trips, the admin's cron command runs across every server (or every server in its allow/deny list). This is the same fanout/auth-bypass class as `NEZHA-002` (cron creation), but reachable by a different code path: even if `/cron` writes are restricted to admin, this `/alert-rule` and `/service` writes are member-reachable and let a member invoke pre-existing admin crons. ## Affected versions Commit `50dc8e660326b9f22990898142c58b7a5312b42a` and earlier on `master`. ## Reachability chain 1. `POST /api/v1/alert-rule` (or `POST /api/v1/service`) is `commonHandler`-gated — any authenticated user. 2. `createAlertRule` / `createService` accepts `FailTriggerTasks` and `RecoverTriggerTasks` from the request body without validating ownership. 3. `validateRule` (`cmd/dashboard/controller/alertrule.go:169-196`) only checks `rule.Ignore` server IDs — not the trigger task IDs. 4. `validateServers` (`cmd/dashboard/controller/service.go:543-549`) only checks the service's `SkipServers` map — not the trigger task IDs. 5. When the alert/service trips: `service/singleton/alertsentinel.go:170, 180` and `service/singleton/servicesentinel.go:747, 750` call `CronShared.SendTriggerTasks(...)`. 6. `SendTriggerTasks` (`service/singleton/crontask.go:113-127`) iterates the requested task IDs against `c.list` and calls `CronTrigger(c, triggerServer)()` for each — no ownership check. 7. `CronTrigger` then fans the cron's `Command` to every connected agent (per `Cover` rules). ## Code locations ```go // cmd/dashboard/controller/alertrule.go:47-77 func createAlertRule(c *gin.Context) (uint64, error) { var arf model.AlertRuleForm var r model.AlertRule if err := c.ShouldBindJSON(&arf); err != nil { return 0, err } uid := getUid(c) r.UserID = uid r.Name = arf.Name r.Rules = arf.Rules r.FailTriggerTasks = arf.FailTriggerTasks // <-- attacker-controlled task IDs r.RecoverTriggerTasks = arf.RecoverTriggerTasks // <-- ditto r.NotificationGroupID = arf.NotificationGroupID enable := arf.Enable r.TriggerMode = arf.TriggerMode r.Enable = &enable if err := validateRule(c, &r); err != nil { return 0, err } // only checks rule.Ignore servers ... } ``` ```go // cmd/dashboard/controller/alertrule.go:169-196 func validateRule(c *gin.Context, r *model.AlertRule) error { if len(r.Rules) > 0 { for _, rule := range r.Rules { if !singleton.ServerShared.CheckPermission(c, maps.Keys(rule.Ignore)) { return singleton.Localizer.ErrorT("permission denied") } // ... duration/cycle validation only } } // BUG: no check on r.FailTriggerTasks or r.RecoverTriggerTasks ownership. return nil } ``` ```go // service/singleton/crontask.go:113-127 func (c *CronClass) SendTriggerTasks(taskIDs []uint64, triggerServer uint64) { c.listMu.RLock() var cronLists []*model.Cron for _, taskID := range taskIDs { if c, ok := c.list[taskID]; ok { // <-- looks up ANY cron in global state cronLists = append(cronLists, c) } } c.listMu.RUnlock() // BUG: no ownership check between alert.UserID and cron.UserID before invoking. for _, c := range cronLists { go CronTrigger(c, triggerServer)() } } ``` ```go // service/singleton/crontask.go:138-181 — CronTrigger return func() { if cr.Cover == model.CronCoverAlertTrigger { // alert-only: only sends to triggerServer (the member's server, when alert was triggered by it) if s, ok := ServerShared.Get(triggerServer[0]); ok && s.TaskStream != nil { s.TaskStream.Send(&pb.Task{Id: cr.ID, Data: cr.Command, Type: model.TaskTypeCommand}) } return } // For Cover=CronCoverAll or CronCoverIgnoreAll: fan out to every server. for _, s := range ServerShared.Range { if cr.Cover == model.CronCoverAll && crIgnoreMap[s.ID] { continue } if cr.Cover == model.CronCoverIgnoreAll && !crIgnoreMap[s.ID] { continue } if s.TaskStream != nil { s.TaskStream.Send(&pb.Task{Id: cr.ID, Data: cr.Command, Type: model.TaskTypeCommand}) } } } ``` ## PoC Pre-conditions: attacker has `RoleMember` credentials. Admin has at least one pre-existing cron with `Cover=CronCoverAll` or `Cover=CronCoverIgnoreAll` (i.e., a "run on all servers" maintenance cron — common in monitoring deployments). Step 1: Enumerate admin cron IDs by ID-guessing. Try IDs 1..N; create AlertRule referencing each, see if the alert handler accepts. Step 2: Create an alert rule referencing the admin's cron and pointed at an offline-trigger condition on the member's own server. ```bash TOKEN=$(curl -sX POST -H 'Content-Type: application/json' \ -d '{"username":"member","password":"hunter2"}' \ http://nezha.example.com/api/v1/login | jq -r .token) curl -sX POST -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \ -d '{"name":"trip","rules":[{"type":"offline","duration":3,"min":1.0,"cover":"member-server-id"}],"fail_trigger_tasks":[1,2,3,4,5],"recover_trigger_tasks":[],"notification_group_id":0,"trigger_mode":0,"enable":true}' \ http://nezha.example.com/api/v1/alert-rule ``` Step 3: Stop the agent on the member's own server (or unplug it). The alert trips after `duration` seconds. `SendTriggerTasks([1,2,3,4,5], member-server-id)` runs. Step 4: For each cron ID in the list, if that cron exists in the global registry and has `Cover=CronCoverAll/IgnoreAll`, its `Command` runs on every server. The same chain works via `POST /api/v1/service` (service-monitor with `fail_trigger_tasks`). ## Composability with NEZHA-002 If `NEZHA-002` is unfixed, this chain is redundant — the member already has direct cron-create access. With `NEZHA-002` fixed, this still gives the member a means to invoke any **pre-existing** admin cron with the member's chosen trigger condition. The fix surface is also independent (alertrule/service write paths, not /cron writes). ## Suggested fix In `validateRule` (and `validateServers`): ```go if !singleton.CronShared.CheckPermission(c, slices.Values(r.FailTriggerTasks)) { return singleton.Localizer.ErrorT("permission denied") } if !singleton.CronShared.CheckPermission(c, slices.Values(r.RecoverTriggerTasks)) { return singleton.Localizer.ErrorT("permission denied") } ``` Defense-in-depth in `SendTriggerTasks`: enforce that `task.UserID == alert.UserID || alertOwnerIsAdmin || taskOwnerIsAdmin`. ## Severity - PR:L because RoleMember credentials needed. - AC:H because attacker has to ID-guess admin cron IDs and have an alert-trip vector. (For a deployment where the attacker has visibility into max cron ID via UI hints or the `id`-query echo, AC drops to L.) - S:C because the cron command runs on every connected agent (different trust zone). - **Auth:** authenticated `RoleMember`. ## Reproduction environment - Tested against: `nezhahq/nezha` master @ `50dc8e660326b9f22990898142c58b7a5312b42a`. - Code locations: - `cmd/dashboard/controller/alertrule.go:47-77` (createAlertRule), 91-131 (updateAlertRule), 169-196 (validateRule) - `cmd/dashboard/controller/service.go:404-445` (createService), 459-509 (updateService), 543-549 (validateServers) - `service/singleton/crontask.go:113-127` (SendTriggerTasks), 133-181 (CronTrigger) - `service/singleton/alertsentinel.go:170, 180` (alert-fire callsite) - `service/singleton/servicesentinel.go:742-750` (service-fire callsite) ## Reporter Eddie Ran. Filed via reporter API. Companion to NEZHA-001/002 — same auth-bypass class but a different write path.
- Source published
- 23 May 2026 00:08:45 UTC
- Source modified
- 26 Jun 2026 21:45:24 UTC
- Observed here
- 24 Oct 2026 13:19:41 UTC
- Source terms
- GitHub Advisory Database · CC-BY-4.0
Original source record · Cite this exact observation · Complete original publisher JSON
This view projects the publisher's source fields for reading; the original JSON retains additional fields and CVE enrichment containers. Attribution: osv · GitHub Advisory Database · CC-BY-4.0. Source text is retained; field grouping and change labels are provided by ChangeIntel.
CVSS_V3 · CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:L
Publisher rating · MODERATE
osv · GO-2026-5645
PublishedNezha Monitoring: RoleMember can fire other users' cron tasks via AlertRule.FailTriggerTasks (no ownership check) in github.com/nezhahq/nezha
- Source published
- 25 Jun 2026 22:34:38 UTC
- Source modified
- 25 Jun 2026 23:01:36 UTC
- Observed here
- 24 Oct 2026 13:19:41 UTC
- Source terms
- OSV · upstream source license applies
Original source record · Cite this exact observation · Complete original publisher JSON
This view projects the publisher's source fields for reading; the original JSON retains additional fields and CVE enrichment containers. Attribution: osv · OSV · upstream source license applies. Source text is retained; field grouping and change labels are provided by ChangeIntel.