GHSA-rxf6-wjh4-jfj6: Nezha Monitoring: RoleMember can fire other users' cron tasks via AlertRule.FailTriggerTasks (no ownership check)
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 versionsCommit `50dc8e660326b9f22990898142c58b7a5312b42a` and earlier on `master`.Reachability chain`POST /api/v1/alert-rule` (or `POST /api/v1/service`) is `commonHandler`-gated — any authenticated user.`createAlertRule` / `createService` accepts `FailTriggerTasks` and `RecoverTriggerTasks` from the request body without validating ownership.`validateRule` (`cmd/dashboard/controller/alertrule.go:169-196`) only checks `rule.Ignore` server IDs — not the trigger task IDs.`validateServers` (`cmd/dashboard/controller/service.go:543-549`) only checks the service's `SkipServers` map — not the trigger task IDs.When the alert/service trips: `service/singleton/alertsentinel.go:170, 180` and `service/singleton/servicesentinel.go:747, 750` call `CronShared.SendTriggerTasks(...)`.`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.`CronTrigger` then fans the cron's `Command` to every connected agent (per `Cover` rules).Code locationsPoCPre-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.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-002If `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 fixIn `validateRule` (and `validateServers`):Defense-in-depth in `SendTriggerTasks`: enforce that `task.UserID == alert.UserID || alertOwnerIsAdmin || taskOwnerIsAdmin`.SeverityPR: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 environmentTested 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)ReporterEddie Ran. Filed via reporter API. Companion to NEZHA-001/002 — same auth-bypass class but a different write path.
CVSS v3.1: 5.4