# Nezha's authenticated agents can forge service-monitor results for other users' services

Nezha's service-monitor worker accepts agent TaskResults for any existing service ID, letting a low-privilege agent forge monitoring history and state for other tenants' services.

- Payout: Research
- Severity: high
- Program: Nezha
- Found by: sondt99
- Disclosed: 2026-06-01
- CVE: CVE-2026-48119
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-4g6j-g789-rghm
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-4g6j-g789-rghm
- Tags: area:web domain:web impact:integrity kind:research platform:ghsa sev:high tech:go tech:grpc technique:source-review vuln:access-control vuln:auth-bypass vuln:business-logic

**Target:** gRPC agent task stream / service-monitor result worker
**Root cause:** The inbound TaskResult worker only checks that the reported service ID exists; it never verifies the reporter server is covered by the service, owned by the service owner, or was actually assigned the task, unlike the outbound dispatch path which enforces Service.Cover, SkipServers and ownership.
**Steps:**
1. Authenticate to the gRPC RequestTask stream with a valid `client_secret` and `client_uuid`; unknown valid UUIDs get registered as a server for the secret owner (`service/rpc/auth.go:63-75`).
2. Send a `TaskResult` with attacker-controlled `id`, `type`, `delay`, `data`, `successful` (`proto/nezha.proto:60-65`), setting `id` to a victim service ID.
3. Stream handler dispatches service-monitor result types to `ServiceSentinelShared.Dispatch` with `Reporter: clientID` (`service/rpc/nezha.go:85-90`).
4. Worker at `service/singleton/servicesentinel.go:475-483` resolves `r.Data.GetId()` to an existing service and records history keyed by ServiceID/Reporter without authorization checks.
5. Result updates today's status, current state and can trigger victim notifications containing attacker-controlled `mh.Data`.
**How found:** Compared the outbound dispatch path (which enforces Service.Cover, SkipServers, canSendTaskToServer and owner/server ownership checks) against the inbound result worker, noticing the inbound path only validated that the service ID existed.
**Exploitation:** A low-privilege user with a normal account, a valid agent secret and one registered agent submits forged TaskResult messages over the authenticated gRPC stream naming another tenant's service ID; the sentinel records fake ServiceHistory rows, poisons availability/current-state data, and can fire victim-owned notifications with attacker-supplied text. No dashboard admin privileges required.
**Insight:** When a system has both an outbound task-dispatch path and an inbound result path, diff their authorization checks: outbound often enforces coverage/ownership while the result handler trusts the reporter, so look for result-submission endpoints that key only on the target object ID.

## Proof of concept (copied exactly from the original report)

**Dispatch forged TaskResult from attacker reporter**

```
ss.Dispatch(ReportData{
		Reporter: 2,
		Data: &pb.TaskResult{
			Id:         10,
			Type:       model.TaskTypeTCPPing,
			Delay:      12,
			Data:       "forged result from unauthorized agent",
			Successful: true,
		},
	})
```

