* fix: prevent unbounded transfer history growth
* fix: preserve transfer history on alert load errors
* test: close transfer cleanup database
---------
Co-authored-by: naiba <hi@nai.ba>
* test: TDD regression tests for GHSA-jx78-55p5-rwv5 stream quota enforcement
* Apply remaining changes
* fix: update action SHA allowlist and test assertions to match dependabot bump
* fix: close GHSA-jx78-55p5-rwv5 incomplete fix of GHSA-qjpp-gffx-2wm9
Finding 1 (Moderate): nil-guard reporterServer in delayCheck and notifyCheck.
ServerShared has its own lock independent of serviceResponseDataStoreLock, so
m := ServerShared.GetList() taken inside the worker can return a nil entry for
the reporter if the server was concurrently deleted. Previously this caused an
unrecovered SIGSEGV in the worker goroutine (and in the gRPC layer with no
recovery interceptor), taking down the whole instance.
Finding 2 (Low): nil-guard ss.services[id] in ServiceSentinel.Delete().
A caller-supplied id that is absent from the registry caused
ss.services[id].CronJobID to panic, aborting the Delete loop and leaving every
subsequent valid id as a zombie service (DB row deleted, in-memory entry kept,
cron probe still running).
Regression tests added for both findings following the existing
servicesentinel_lifecycle_test.go patterns.
* Apply remaining changes
* chore: replace commit hashes with version tags in test.yml
* fix(server): serialize authoritative lifecycle changes
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
* fix(service): bind reports to reporter lifecycle
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
* fix(rpc): reject results from stale task streams
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
* fix(agentcompat): allow version-tagged actions
* fix(agentcompat): allow literal checkout refs
* refactor(agentcompat): remove SHA resolver policy
* test(agentcompat): remove resolver SHA fixtures
* test(agentcompat): remove mutable ref fixtures
* test(agentcompat): use tagged actions in secure fixtures
* test(agentcompat): update credential fixtures for tags
* test(agentcompat): update reusable action fixtures
* test(agentcompat): update artifact redaction fixtures
* test(agentcompat): finish artifact fixture tag migration
* test(agentcompat): update workflow validation fixtures
* test(agentcompat): update dependency workflow fixture
* ci(agentcompat): stop pinning cross-repository revisions
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: naiba <hi@nai.ba>
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
GET /api/v1/ddns and /api/v1/notification returned full objects with
plaintext credentials (Cloudflare/TencentCloud secrets, webhook URLs
with embedded bot tokens, Authorization headers). Redact these fields
in the list responses.
Since the frontend edit form repopulates from the list endpoint, the
update handlers now treat an empty submitted credential as "no change"
and preserve the stored value, preventing accidental secret wipes.
Ref: GHSA-ww5p-j6cj-6mqq
The v2.2.2 Release workflow failed: fetch-frontends.sh fetched user-dist
from hamster1963/nezha-dash-v2 at v2.1.1, but upstream went v2.1.0 -> v2.1.2
and never tagged v2.1.1, so the dist.zip download 404'd and unzip aborted the
build. Point at the existing v2.1.2 asset.
Add TestCheckStatus_SampleMemoryBounded asserting that, across 100k ticks,
the per-(alert,server) sample slice length and capacity stay bounded by the
rule's retention window and never grow with elapsed time.
Trim alert samples using AlertRule.RetentionWindow() (max rule duration)
instead of Check()'s verdict max, which is 0 while a rule's window is still
filling. The old max<=0 trim wiped history every tick, so Duration>=2 general
rules never accumulated enough samples and never fired a notification.
Add model + end-to-end singleton regression tests covering sample
accumulation, the retention-window contract, and the actual notify path.
Decouple OAuth2 callback host from agent install host. When dashboard_host
is empty, the request Host is passed through; otherwise non-reserved hosts
are pinned to dashboard_host. Added to reserved-host allowlist for NAT.
After #1197/#244 a new agent emits both hyphenated and underscore credential
metadata, so a new dashboard receives both at once. Add tests for the everyday
both-present case and pin that the hyphenated key takes precedence over the
underscore alias that Caddy v2.11.4 strips.
A general alert rule with Duration:0 (accepted by the API, no minimum
validation) drove fail*100/total with total==0, panicking with an integer
divide-by-zero inside checkStatus, which has no recover and would take down the
whole alert goroutine — a config-reachable DoS by any user able to create alert
rules. Skip duration<=0 rules in Check via continue (not boundCheck, which would
pollute hasPassedRule and skip sibling valid rules). Also trim the sample slice
when max<=0 in alertsentinel, otherwise it appended every tick without ever
trimming. Adds regression tests for zero-only and mixed rule sets.
GHSA-jg62-j5h6-8mpq: the terminal and file-manager endpoints created unbounded
IO streams; an authenticated member could open thousands, each spawning
goroutines, a 1MiB buffer and an agent-side PTY, exhausting dashboard and agent
resources. CreateStream now enforces a per-user (20) and per-server (40) cap in
the existing ioStreamMutex critical section, using the stream map as the single
source of truth. Dashboard-internal streams (uid==0: NAT, server transfer, MCP
transfer) skip the per-user cap but still count per-server. Adds caps,
exemption, slot-release and no-leak regression tests.
Completes the EnableShowInService->HideForGuest rename; the public stats
filter was missed in the previous commit, leaving service/singleton
referencing the removed field and breaking the build.
Upstream hamster1963/nezha-dash-v2 v2.0.4 reviewed commit-by-commit for
supply-chain risk (dependency bumps only, no new packages/postinstall
scripts/network calls/obfuscation) and mirrored to nezhahq/user-frontend-backup.
Co-authored-by: cloudcode <cloudcode@users.noreply.github.com>
GHSA-x6fg-52vr-hj4w: NAT is a commonHandler any authenticated member can
create, and newHTTPandGRPCMux matches r.Host against NAT before dispatching
the dashboard/gRPC handlers, so a member could register the dashboard's own
host as a NAT Domain and hijack global routing. Add IsReservedDashboardHost
(InstallHost/ListenHost plus an operator-declared ReservedHosts list for
reverse-proxy deployments), reject reserved hosts on create/update, and drop
pre-planted records when building the startup cache.
GHSA-39g2-8x68-pmx8: bind-time CheckPermission let a member pre-bind a DDNS
profile ID that the victim would only create later. GetDDNSProvidersFromProfiles
now re-validates ownership by server owner UID and skips foreign-owned
profiles (UserID==0 is treated as a migration artifact, not an admin grant).
Co-authored-by: cloudcode <cloudcode@users.noreply.github.com>
Introduce Personal Access Tokens (nzp_*) as a stateless auth path alongside
JWT, gated per-endpoint by a scope middleware (nezha:{resource}:{verb}) with
fail-closed empty-scope defaults and a server-id whitelist. Self-management
endpoints (profile, api-tokens, oauth2 bind, refresh-token) explicitly reject
PATs to block privilege-escalation chains. A revoke registry tears down active
long-lived connections (terminal, fm, ws, transfer, mcp) the moment a PAT is
deleted, with a tombstone closing the revoke->register race.
Add an MCP endpoint that proxies tool calls (exec, fs read/write/delete,
transfer) to agents over gRPC, guarded by origin/DNS-rebinding checks, a
per-token rate limiter, audit logging, and a kill switch. Serialize all
sends through the IOStream wrapper to honour grpc-go's concurrency contract.
Add CSRF double-submit protection on unsafe cookie-authenticated methods,
exempting authenticated PAT requests by context identity (not a forgeable
Authorization header). Apply visibility/whitelist filtering consistently
across list, get-by-id, and mutate paths to enforce tenant isolation.
Migrate legacy mcp:* scopes: rewrite read/exec to nezha:* equivalents and
drop dangerous write/delete/wildcard grants.
Co-authored-by: cloudcode <cloudcode@users.noreply.github.com>
Replace the {user_id, ip} claim pair with {keyId, uid}:
- keyId is a 32-byte random id that points to a row in the new
jwt_sessions table holding the real user id, bound IP, UA hash,
TokenVersion and expiry.
- uid is the user id encoded through pkg/idcodec; mismatch between
claim uid and session.UserID trips WAF block on the caller IP.
- identityHandler now rejects unknown/revoked/expired sessions, IP
drift and stale TokenVersion. Refresh updates session.ExpiresAt.
User.TokenVersion bumps on password change and revokes outstanding
sessions, so a leaked JWT secret alone is no longer enough to forge
a token. JWTSession rows are GC'd every 10 minutes (expired + grace
or revoked >24h). OAuth2 callback shares the same issue path.
Includes regression tests for happy path, mismatched claim uid,
revoked session, TokenVersion bump, IP drift and unknown keyId.
Co-authored-by: cloudcode <cloudcode@users.noreply.github.com>
NewServerTransferClass merged Verified and acked-rollback rows and sorted
them by AckedAt only. On Windows time.Now() granularity is ~15.6ms, so
MarkVerified and an immediately-following MarkRevertDelivered routinely
share a timestamp. The stable sort then left the Verified candidate
ahead of the rollback that is actually on disk and the agent was locked
out on the next restart. Add transferID as a deterministic tiebreaker so
the later rotation always wins.