Commit Graph
100 Commits
Author SHA1 Message Date
naibaandnaiba/CloudCode d96b34b0d4 ci: require agentcompat stress validation
Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
2026-07-20 04:55:20 +00:00
naibaandnaiba/CloudCode d501f23473 test(agentcompat): add exact stress scenario
Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
2026-07-20 04:54:03 +00:00
naibaandnaiba/CloudCode 64daa6e9ae test(agentcompat): add scenario CLI wiring
Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
2026-07-20 04:52:28 +00:00
naibaandnaiba/CloudCode 9af3720bf0 test(agentcompat): add integration scenarios
Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
2026-07-20 04:51:20 +00:00
naibaandnaiba/CloudCode b3b3d92895 test(agentcompat): add evidence validation
Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
2026-07-20 04:50:03 +00:00
naibaandnaiba/CloudCode 1c3e926819 test(agentcompat): add protocol clients
Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
2026-07-20 04:48:15 +00:00
naibaandnaiba/CloudCode 8a0e84b214 test(agentcompat): add dashboard runtime harness
Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
2026-07-20 04:46:55 +00:00
naibaandnaiba/CloudCode 2a3245c412 test(agentcompat): add agent runtime harness
Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
2026-07-20 04:45:05 +00:00
naibaandnaiba/CloudCode 3a9e0c7887 test(agentcompat): add process supervision harness
Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
2026-07-20 04:43:28 +00:00
naibaandnaiba/CloudCode 0543995ec3 test(agentcompat): harden deterministic fixtures
Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
2026-07-20 04:42:03 +00:00
naibaandnaiba/CloudCode fcf58b0b4b test(agentcompat): define integration contracts
Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
2026-07-20 04:39:48 +00:00
naibaandnaiba/CloudCode 391f622c75 test(agentcompat): cover SQLite hold arbitration
Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
2026-07-20 04:37:59 +00:00
naibaandnaiba/CloudCode 52c766d27c test(rpc): cover reentrant state observers
Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
2026-07-20 04:36:50 +00:00
naibaandnaiba/CloudCode aee586baad feat(agentcompat): add dashboard listener hooks
Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
2026-07-20 04:33:38 +00:00
naibaandnaiba/CloudCode 3f6de265a0 fix(mcp): harden dashboard dispatch lifecycle
Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
2026-07-20 04:32:16 +00:00
naibaandnaiba/CloudCode 2640b86d3a feat(agentcompat): expose dashboard capability routes
Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
2026-07-20 04:30:50 +00:00
naibaandnaiba/CloudCode 0c6eb416a5 feat(agentcompat): add SQLite hold scoped attribution
Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
2026-07-20 04:29:21 +00:00
naibaandnaiba/CloudCode 26e92da33b feat(agentcompat): add scoped IO stream capabilities
Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
2026-07-20 04:27:33 +00:00
naibaandnaiba/CloudCode c756ef9385 fix(rpc): make IO stream lifecycle race-safe
Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
2026-07-20 04:26:19 +00:00
naibaandnaiba/CloudCode 8b47ff141f feat(agentcompat): add shared runtime contracts
Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
2026-07-20 04:25:03 +00:00
naibaandnaiba/CloudCode a416e3bf29 test(compat): complete NAT fixture shutdown
Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
2026-07-14 06:13:57 +00:00
naibaandnaiba/CloudCode be9af6fcb3 test(compat): harden fixture containment cleanup
Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
2026-07-14 06:12:00 +00:00
naibaandnaiba/CloudCode 92a1cc76be test(compat): add deterministic local fixtures
Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
2026-07-14 05:49:15 +00:00
naibaandnaiba/CloudCode b01ff45ef4 chore(frontend-templates): bump user-dist v2.4.1 and aobobo-dist v1.4.6
Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
2026-07-12 06:27:58 +00:00
naibaandnaiba/CloudCode 1477138dad chore(frontend-templates): bump admin-dist v2.2.5, user-dist v2.3.2 and aobobo-dist v1.4.5
Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
2026-07-04 11:08:03 +00:00
naibaandnaiba/CloudCode fd31e5f1b0 chore(frontend-templates): add aobobo template
Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
2026-06-28 01:09:23 +00:00
naibaandnaiba/CloudCode e1045ffd30 fix(api): redact profile password hash
Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
2026-06-28 01:09:23 +00:00
naiba 6aab2e4919 Merge pull request #1213 from nezhahq/dependabot/github_actions/github-actions-02325a8da5
chore(deps): bump the github-actions group with 2 updates
2026-06-27 11:30:04 +08:00
naibaandnaiba/CloudCode d750fa035d chore(frontend-templates): bump user-dist v2.3.1
Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
2026-06-20 11:01:54 +00:00
naiba d556c3216f chore(frontend-templates): bump user-dist v2.3.0 and nazhua-dist v1.1.0 2026-06-20 00:40:38 +00:00
naiba 8cd2da79b8 chore(frontend-templates): bump admin-dist v2.2.4 2026-06-20 00:39:47 +00:00
naiba 874a47c32c Merge pull request #1211 from hamster1963/fix-service
Filter service stats based on user permissions
2026-06-20 08:25:15 +08:00
naiba 3d74cd9431 fix(alert): retain Duration samples for offline rules so they fire
RetentionWindow() trimmed offline-rule samples to 1, but Check's offline
branch reads points[len-Duration:] and needs Duration samples. The window
never filled, boundCheck always returned "passed", and offline alerts never
fired a notification — while other alert types worked. Offline rules now
retain Duration samples (cycle rules still keep 1, matching their last-sample
lookback).

Fixes the prior over-correction in RetentionWindow; corrects the test that
had pinned the buggy offline=1 value, and adds offline + combined-rule
regression tests driving the real trim loop.
2026-06-08 00:57:14 +00:00
naiba 576e6773c0 fix(release): bump user-dist to v2.1.2 (v2.1.1 never released upstream)
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.
2026-06-07 16:23:24 +00:00
naiba b12b1709d9 test(alert): pin sample-slice memory-boundedness invariant
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.
2026-06-07 16:14:31 +00:00
naiba e756b4540c fix(alert): keep sample history by rule-defined retention window
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.
2026-06-07 16:11:43 +00:00
naiba 66de244c2e feat(oauth2): add dashboard_host setting for callback URL
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.
2026-06-06 05:03:12 +00:00
naiba c595728609 chore(frontend-templates): bump admin-dist v2.2.2 and user-dist v2.1.1 2026-06-06 04:55:31 +00:00
naiba 6e9510d26a chore(frontend-templates): bump admin-dist v2.2.0 2026-06-05 02:42:58 +00:00
naiba 5fd91c44b0 test(rpc): cover both-key-styles present in agent auth metadata
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.
2026-06-05 02:42:58 +00:00
naiba 0759f5c801 fix(alert): prevent divide-by-zero panic and unbounded growth on zero-duration rule
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.
2026-06-05 02:42:49 +00:00
naiba 36240f5888 fix(rpc): cap concurrent IO streams per user and per server
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.
2026-06-05 02:42:38 +00:00
naiba fb5c37e983 fix(oauth2): validate Host header against allowlist for callback URL
GHSA-9rc6-8cjv-rcvx: getRedirectURL derived the OAuth2 callback URL from the
raw Host header, so a forged Host (or a provider with loose redirect-URI
matching) could divert a victim's authorization code to an attacker origin and
bind their identity. Trust the request Host only when it is an operator-declared
dashboard host (IsReservedDashboardHost, same allowlist guarding NAT routing);
otherwise fall back to the configured InstallHost.
2026-06-05 02:42:28 +00:00
naiba 1927b0c6a4 chore(ci): add dependabot version updates for gomod and actions
Repo only had Dependabot security updates (reactive). Add weekly grouped
version updates for Go modules and GitHub Actions to keep dependencies
current and reduce CVE accumulation.
2026-06-05 01:08:00 +00:00
naiba 78f5b014ee fix(csrf): set Secure on csrf cookie only over HTTPS
CodeQL go/cookie-secure-not-set (CWE-614) flagged the nz-csrf cookie as
missing the Secure attribute. Mirror writeOauth2StateCookie and derive
Secure from the request scheme instead of hardcoding it: forcing
Secure=true would make browsers drop the cookie on plain-HTTP intranet
deployments, breaking the double-submit CSRF pair and 403-ing every
unsafe request.
2026-06-05 01:08:00 +00:00
naiba c3c361ad66 fix(service): flip CopyStats public filter to HideForGuest
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.
2026-06-02 03:32:10 +00:00
naiba e47b5b99f8 chore(frontend-templates): bump admin-dist v2.1.2 and user-dist v2.1.0 2026-06-02 03:31:36 +00:00
naiba 8285a3d99f docs(mcp): note server.exec reaps the whole process tree
Document in the tool description and protocol comment that the agent
kills the entire process group/JobObject on return or timeout, so
background jobs must fully detach (setsid/screen/tmux/systemd-run).
2026-06-02 03:31:36 +00:00
naiba 836f1db4b7 feat(service): replace EnableShowInService with HideForGuest
Mirror the server HideForGuest flag on services: rename the field
(dropping the gorm default), invert userCanViewService so a service is
visible to guests by default and hidden only when HideForGuest is set,
and flip the public-stats filter in servicesentinel accordingly. Update
the visibility and permission-matrix tests to the new semantics.
2026-06-02 03:31:36 +00:00
naiba bb38a9d4d2 fix(mcp): accept agents reporting version without v prefix and stop masking tool errors
compareSemver fell back to lexical string ordering when the numeric
segments were equal, so an agent reporting "2.1.0" compared as older than
the gate constant "v2.1.0" (0x32 < 0x76). This wrongly flagged every agent
as unsupported_agent, breaking server.exec and fs.* on all online servers.
Drop the fallback: semverParts already strips the v prefix, so equal
numeric segments mean equal versions.

The error was further masked because tool error responses shipped a
structuredContent {error_code,error} that violates the tool outputSchema
(which requires exit_code/stdout/...). Strict MCP clients validated it and
rejected the whole response with -32602, hiding the real isError text.
Omit structuredContent on error; the cause stays in content[].text.
2026-05-31 16:18:57 +00:00
naiba 9b5199ac6c chore(frontend-templates): bump admin-frontend to v2.1.1 2026-05-31 15:14:48 +00:00
naiba 083bc985c5 feat(auth): split inventory scope out of server scope
Carve a new nezha:inventory:{read,delete,*} resource family out of
nezha:server:*. Listing and deleting servers/server-groups (GET /server,
/server-group, /ws/server, batch-delete/server[-group], MCP server.list)
now require nezha:inventory:*, while nezha:server:* covers per-server
runtime operations (get, exec, fs, config, metrics, batch-move,
force-update).

Also declare MCP tool OutputSchema for exec/fs/meta/server/transfer and
wrap server.list output in {servers,count} so strict MCP clients accept
the structured result. Correct the /file all-of scope entry and the stale
server:read documentation.
2026-05-31 15:14:41 +00:00
naiba f7f8264ec0 fix(auth): always serialize User.Role so admin (role 0) is not omitted
The Role field used json:"role,omitempty". Admin is RoleAdmin = 0, so an
admin profile serialized without a `role` key. The admin-frontend gates the
admin menu (user management, settings) on `role === 0`, and its normalizeRole
helper defaults a missing role to non-admin, so admins lost the admin menu.

Drop omitempty so role 0 is always sent. Add a regression test.
2026-05-31 12:05:11 +00:00
naibaandcloudcode 34ab8a31bc chore(frontend-templates): bump admin-frontend to v2.1.0
Embed the admin-frontend v2.1.0 release (PAT UI, CSRF handling, reserved
hosts) so the upcoming backend v2.1.0 ships the matching frontend.

Co-authored-by: cloudcode <cloudcode@users.noreply.github.com>
2026-05-31 10:26:41 +00:00
naibaandcloudcode 7432efde04 chore(frontend-templates): bump user-frontend to v2.0.4
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>
2026-05-31 09:49:57 +00:00
naibaandcloudcode 401713bab5 ci(gosec): silence G101 false positives on PAT context keys and scope const
gosec G101 flags apiTokenCtxKey, apiTokenLastUsedCtxKey, and the
ScopeNotificationGroupWrite identifier as hardcoded credentials. They are
context-key names and a scope string, not secrets. Annotate them with
#nosec G101 (matching the existing JWTSecretEnvKey precedent) so the
gosec CI step passes without disabling the rule globally.

Co-authored-by: cloudcode <cloudcode@users.noreply.github.com>
2026-05-31 08:16:55 +00:00
naibaandcloudcode 18c044d959 fix(security): set real IP from peer in ConfigUsePeerIP gRPC mode
In ConfigUsePeerIP mode ctxWithRealIP validated connectingIp but never
assigned it to ip, so CtxKeyRealIP was left empty. model.CheckIP and
model.BlockIP short-circuit on an empty IP, which silently disabled the
gRPC WAF block table and brute-force token counters for every peer-IP
deployment — including the stream interceptors this path now backs.

Assign ip = connectingIp so the WAF and BlockIP observe the source. The
empty-header path is unchanged (it intentionally opts out of IP WAF).

Add rpc_test.go covering peer-IP IPv4/IPv6, the no-peer error, and the
no-header opt-out so the behaviours cannot be re-coupled.

Co-authored-by: cloudcode <cloudcode@users.noreply.github.com>
2026-05-31 07:45:23 +00:00
naibaandcloudcode b1d84c6b2f fix(security): harden gRPC stream WAF, MCP fs arg validation, and PAT server binding
- ServeRPC now applies real-IP + WAF interceptors to streaming RPCs
  (RequestTask, IOStream), not just unary calls; without them
  authHandler.check saw an empty real IP so brute-force BlockIP counters
  never keyed on a source and the WAF block table was bypassed at the
  stream entrypoint. Factored real-IP resolution into ctxWithRealIP shared
  by unary and stream paths.
- handleFsRead rejects negative offset/length; handleFsWrite validates
  if_match_sha256 is 64 hex chars before dispatch.
- createAPIToken always verifies each server_id exists (even for admins),
  so an admin cannot bind a PAT to a not-yet-created server id that a
  future server would auto-inherit.
- Add relay zero-length-chunk rejection regression test.

Co-authored-by: cloudcode <cloudcode@users.noreply.github.com>
2026-05-31 05:51:32 +00:00
naibaandcloudcode 05e14981b5 fix(security): verify transfer-token HMAC and sign CSRF double-submit cookie
- consumeTransferToken now recomputes and constant-time compares the
  entry's HMAC-SHA256; previously the signature was minted but never
  checked, so the documented "防篡改" guarantee was hollow and security
  rested solely on the sync.Map key's randomness.
- CSRF switches from a raw-random double-submit cookie to a signed token
  (nonce.HMAC-SHA256 keyed by JWTSecretKey). The middleware now also
  validates the signature, defeating sibling-subdomain cookie tossing
  where a naive header==cookie pair would otherwise pass.

Co-authored-by: cloudcode <cloudcode@users.noreply.github.com>
2026-05-31 05:48:50 +00:00
naibaandcloudcode d4837dfc7a fix(security): reserve dashboard hosts from NAT and re-check DDNS profile ownership at worker time
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>
2026-05-31 02:04:35 +00:00
naibaandcloudcode e8dabf5bc6 feat(auth): add PAT auth, scoped REST/MCP access, CSRF, and tenant isolation
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>
2026-05-30 15:56:44 +00:00
029695344c chore(frontend-templates): bump admin-frontend to v2.0.9
Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>

Co-authored-by: cloudcode <cloudcode@users.noreply.github.com>
2026-05-26 07:12:31 +00:00
57703c4c3e ci(gosec): mark jwt secret env var name as non-secret
Gosec G101 treats the environment variable name NZ_JWTSECRETKEY as a hardcoded secret value. Keep the env-first JWT secret behavior unchanged and annotate the constant so CI only suppresses this false positive.

Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>

Co-authored-by: cloudcode <cloudcode@users.noreply.github.com>
2026-05-26 05:27:46 +00:00
naibaandcloudcode c94639e199 fix(server-group): hide guest-empty groups from guest listing
listServerGroup returned every group, including ones whose only
servers are HideForGuest=true (or that have zero members), to
unauthenticated callers. The group name itself is then leaked to
guests even though every server it references is hidden from them.

Guest visitors now only see groups that contain at least one guest-
visible server. Authenticated members and admins keep full visibility
(including their own empty groups) so management UI still works.

Co-authored-by: cloudcode <cloudcode@users.noreply.github.com>
2026-05-26 04:07:50 +00:00
naibaandcloudcode a4f4cb1f34 fix(fm): switch create FM session to POST to defeat CSRF
GET /api/v1/file?id=<server> created an FM stream on the agent stream
and committed real state change (TaskTypeFM dispatched). With JWT
cookie SameSite=Lax a victim's browser would still send the cookie on
a top-level cross-site GET, so an attacker could trick a logged-in
user into opening an FM session on any of their own servers, consuming
resources and triggering the agent's FM machinery without consent.

Mirror the GHSA-8qhj-4f8c-j8qg fix: move the route to POST. SameSite=
Lax cookies are not sent on cross-site POST. Frontend (admin-frontend)
adjusted in a follow-up commit.

Co-authored-by: cloudcode <cloudcode@users.noreply.github.com>
2026-05-26 04:07:49 +00:00
naibaandcloudcode a8f014838c fix(service): hide EnableShowInService=false services from sideband endpoints
GHSA-vrmh-5mmx-hjwx: GET /api/v1/server/:id/service and
GET /api/v1/service/:id/history both iterate the raw service list and
emit ServiceName / timing for any service that happens to monitor the
queried server, ignoring the owner's EnableShowInService=false flag.
Both routes are on optionalAuth, so unauthenticated visitors could
enumerate hidden services by name and timing.

Introduce userCanViewService(c, service):
- EnableShowInService=true     -> always visible
- admin                        -> always visible
- authenticated owner          -> visible (HasPermission)
- everyone else                -> hidden

getServiceHistory rejects unknown-or-invisible service with the same
'service not found' message so the endpoint cannot be used as an
oracle. listServerServices pre-filters the sorted service list.

Owners and admins keep their existing visibility into their own hidden
services.

Tests:
- TestUserCanViewServiceVisibleServiceIsPublic
- TestUserCanViewServiceHiddenServiceRejectsGuest
- TestUserCanViewServiceHiddenServiceRejectsForeignMember
- TestUserCanViewServiceHiddenServiceAllowsOwner
- TestUserCanViewServiceHiddenServiceAllowsAdmin

Co-authored-by: cloudcode <cloudcode@users.noreply.github.com>
2026-05-26 03:59:59 +00:00
naibaandcloudcode 9c0fa84c95 fix(cron): switch manual trigger to POST to defeat CSRF
GHSA-8qhj-4f8c-j8qg: GET /api/v1/cron/:id/manual changed shared state
on the agent stream, and the JWT cookie is SameSite=Lax so a victim's
browser would send the cookie on a top-level cross-site GET. An
attacker could trick a logged-in user into firing any of their own
cron commands.

Switch the route to POST: SameSite=Lax cookies are not sent on cross-
site POST, closing the CSRF window without introducing a new token.

Tests:
- TestCronManualTriggerRejectsCrossSiteGET locks in that GET no longer
  resolves.
- TestCronManualTriggerAcceptsSameSitePOST locks in the legitimate POST
  still works.

Frontend (admin-frontend) updated in a follow-up commit.

Co-authored-by: cloudcode <cloudcode@users.noreply.github.com>
2026-05-26 03:59:50 +00:00
naibaandcloudcode e05700c2f0 feat(jwt): server-side session table with keyId + obfuscated uid claims
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>
2026-05-26 03:51:05 +00:00
naibaandcloudcode d6f381d078 feat(idcodec): add hashid obfuscation derived from jwt secret
New pkg/idcodec wraps sqids with an alphabet derived from the JWT
secret via HKDF-SHA256 (info="nezha/idcodec/alphabet/v1"). Rotating
NZ_JWTSECRETKEY automatically reshuffles the alphabet, which doubles
as a kill switch for outstanding hashids without touching the encoder
itself.

The base alphabet drops visually-confusable characters (0/O/o/I/l/1)
and MinLength=8 hides small integer ids. Decode round-trips through
Encode to reject inputs that decode by accident under the same
alphabet.

Co-authored-by: cloudcode <cloudcode@users.noreply.github.com>
2026-05-26 03:50:36 +00:00
naibaandcloudcode 95c3a775d5 feat(config): make jwt_secret_key env-first and never persist when injected
NZ_JWTSECRETKEY takes priority over config.yaml, env-injected keys are
not persisted and skip the version-driven rotation. When the secret is
auto-generated as a last-resort fallback, write it via a single-field
YAML patch so we never round-trip the in-memory key back to disk.

The JWTSecretKey field is marked json:"-" yaml:"-" so Save() can no
longer leak it. Update ReadEnvFile test to lock in env-over-yaml
precedence.

Co-authored-by: cloudcode <cloudcode@users.noreply.github.com>
2026-05-26 03:50:26 +00:00
naiba d202ecf8db chore(frontend-templates): bump admin-frontend to v2.0.8 2026-05-25 11:42:39 +00:00
naiba b83d93fdc9 fix(server-transfer): break loader sort ties by transferID
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.
2026-05-25 10:36:43 +00:00
naiba 6b88cdb012 feat: server transfer rotation 2026-05-25 10:17:34 +00:00
naiba 79a6dd76f8 Update vulnerability reporting contact method 2026-05-22 22:58:18 +08:00
naibaandnaiba/CloudCode 8d21062c3b fix(rpc): authorize agent task results
Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
2026-05-21 02:02:02 +00:00
naibaandnaiba/CloudCode 0f7af0bcb2 fix(rpc): bind io streams to target agents
Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
2026-05-21 02:01:54 +00:00
naibaandnaiba/CloudCode a5e2b0c36d ci: free gitee release storage before uploads
Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
2026-05-19 05:06:38 +00:00
naibaandnaiba/CloudCode d8f077ffa2 ci: relax gitee release upload timeout
Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
2026-05-19 03:59:48 +00:00
naibaandnaiba/CloudCode d61f953c81 ci: make gitee release sync retryable
Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
2026-05-19 03:17:33 +00:00
naibaandnaiba/CloudCode 1efe2bf053 fix(rpc): allow global agent secret across server owners
Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
2026-05-19 01:58:53 +00:00
naibaandnaiba/CloudCode 9d67134148 fix(controller): filter service transfer stats by viewer
Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
2026-05-19 00:35:02 +00:00
naibaandnaiba/CloudCode fb120074ac chore(security): pin JWT algorithm + SameSite and harden OAuth2 state cookie
- Set GinJWTMiddleware.SigningAlgorithm to "HS256" explicitly so a future
  library default change (or an alg:none confusion attempt) cannot weaken
  token validation. This matches the current gin-jwt default, so behaviour
  is unchanged.
- Set CookieSameSite to Lax: same as the modern-browser default, but
  pinned so server-side intent is clear and CSRF on cross-site POST is
  blocked while top-level GET (OAuth callback) still works.
- Move the nz-o2s OAuth2 state cookie into writeOauth2StateCookie and
  set HttpOnly=true. The frontend does not read this cookie, so HttpOnly
  is strictly an XSS-hardening win with no behaviour change.

JWT Cookie HttpOnly/Secure are intentionally left default for now: the
frontend reads \`!!document.cookie\` to display login state and many
deployments terminate TLS at an upstream proxy — flipping those would
require a coordinated frontend change.

Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
2026-05-18 15:17:59 +00:00
naibaandnaiba/CloudCode 7c493e8f0b fix(rpc): verify gRPC client UUID belongs to the agent secret owner
Auth.Check resolved client_uuid → server.ID via UUIDToID without
checking that the resolved server's UserID matched the user that the
secret was bound to. An agent presenting one user's secret could
target a different user's server UUID and impersonate it — poisoning
monitoring state, triggering alerts, or quietly receiving tasks the
real owner expected.

Add authorizeAgentForUUID: if the UUID is unknown we still allow new
registration bound to the secret owner; if the UUID is known but
points to someone else's server we reject. The rejection message
"client UUID does not belong to the agent secret owner" also helps
operators trace which user's secret has leaked.

Note: this changes runtime behaviour after batch-move/server — the
new owner must reconfigure agents with their own secret. That is the
correct contract; the previous behaviour was a cross-user reporting
bug.

Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
2026-05-18 15:17:37 +00:00
naibaandnaiba/CloudCode 280e34977f fix(rpc): enforce magic ff05ff05 on IOStream init
The inline magic check expressed the *invalid* form as
\`byte0 != 0xff && byte1 != 0x05 && byte2 != 0xff && byte3 == 0x05\`,
relying on && to detect a four-byte mismatch. Because && short-circuits,
any payload whose byte0 happened to be 0xff was treated as a valid magic
even if the remaining bytes did not match — almost every random payload
slipped through and only the stream-UUID layer above stood between a
caller with a valid agent secret and a live IOStream session.

Extract the check into isValidIOStreamMagic stated positively (all four
bytes must match) so short-circuit reasoning cannot reintroduce the bug.

Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
2026-05-18 15:17:18 +00:00
naibaandnaiba/CloudCode 097d4f7382 fix(controller): hide foreign server IDs from forceUpdateServer response
The previous loop only checked ownership inside the
\`server != nil && server.TaskStream != nil\` branch. A foreign online
server returned permission denied for the whole batch while foreign
offline / unknown IDs silently went into the Offline bucket — the
response-shape delta let a RoleMember enumerate other users' online
server IDs by submitting them in batches.

Drop both foreign and unknown IDs silently into the Offline bucket
(without dispatching the upgrade task) so the response is indistinguishable
across those three states.

Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
2026-05-18 15:16:59 +00:00
naibaandnaiba/CloudCode 36297699f5 fix(rpc): bind io_stream sessions to creator to prevent terminal/fm hijack
createTerminal and createFM correctly check server ownership before
issuing a stream UUID, but terminalStream and fmStream only verified
that the UUID existed. Any authenticated user holding a valid stream
UUID could attach to it, gaining the original creator's live shell or
file-manager session — and the UUID is exposed via URL path (referer
leaks, access logs, browser history, frontend error reporters).

Bind the creator user ID into ioStreamContext at CreateStream time,
expose StreamOwnership and IsStreamAuthorizedForUser, and check
ownership in terminalStream/fmStream before the WebSocket upgrade so a
rejected attempt does not tear down the legitimate stream via defer.

NAT streams are also routed through CreateStream(_, 0); they are not
reachable from /ws/terminal or /ws/file so a sentinel user ID is fine.

Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
2026-05-18 15:16:40 +00:00
naibaandnaiba/CloudCode ea7ad67f03 fix(ddns): apply SSRF defense to webhook provider
GHSA-6x26-5727-rrm9: a low-privilege member could point a DDNS webhook
at internal or loopback hosts and the dashboard would dial them with the
unrestricted utils.HttpClient.

Extract the notification SSRF defenses (CIDR blocklist, IP-pin DialContext,
SNI preservation, redirect rejection) into reusable helpers in pkg/utils
(NewRestrictedHTTPClient / ResolveAllowedHTTPURL / buildRestrictedHTTPClient)
and route the DDNS webhook through the same path. Replace the notification
inline implementation with a thin wrapper to keep behaviour identical.

Side improvements collected by the refactor:
- prepareRequest now resolves DNS once and returns the paired client, so
  the dialer's pinned IP and the validated URL stay in sync (no more
  double resolution between prepareRequest and SetRecords).
- response body is drained and closed.
- HttpClient / HttpClientSkipTlsVerify are explicitly tagged unsafe for
  attacker-controlled URLs.

Tests cover: hermetic SNI preservation, redirect rejection, dial pin to
the vetted IP, the full blocked-CIDR list at the webhook entry point,
and the verifyTLS↔skipVerifyTLS inversion in the notification wrapper.

Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
2026-05-18 15:14:27 +00:00
naibaandnaiba/CloudCode b2294f11dd fix(ws): scope server stream visibility to viewer ownership
Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
2026-05-17 03:41:28 +00:00
naibaandnaiba/CloudCode 2b6a1430ff chore(deps): upgrade go modules to latest
Bumps direct deps: VictoriaMetrics v1.134.0 -> v1.143.0,
gin-contrib/pprof v1.5.3 -> v1.5.4, knadh/koanf/v2 v2.3.3 -> v2.3.4,
libdns/he v1.2.1 -> v1.2.2, tidwall/gjson v1.18.0 -> v1.19.0,
golang.org/x/crypto v0.49.0 -> v0.51.0,
golang.org/x/net v0.52.0 -> v0.54.0,
google.golang.org/grpc v1.79.3 -> v1.81.1, plus transitive updates.

VictoriaMetrics v1.143.0 requires go >= 1.26.3, so the go directive
is bumped from 1.26 to 1.26.3.

Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
2026-05-17 02:40:09 +00:00
naibaandnaiba/CloudCode 854e2edf17 chore: bump admin frontend to v2.0.7
Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
2026-05-17 10:24:19 +08:00
naibaandnaiba/CloudCode 7061651587 test: cover multi-user permission boundaries
Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
2026-05-17 10:24:19 +08:00
naibaandnaiba/CloudCode 2573ba7522 fix(controller): restrict waf and online-user listings to admins
Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
2026-05-17 10:24:19 +08:00
naibaandnaiba/CloudCode 0ca7ee38c0 fix(controller): filter listing endpoints by ownership
Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
2026-05-17 10:24:19 +08:00
naibaandnaiba/CloudCode 2a65a7db80 fix(controller): enforce ownership on notification group, NAT server, and batch move
Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
2026-05-17 10:24:19 +08:00
naibaandnaiba/CloudCode 58f98db1a0 fix(controller): validate trigger task ownership
Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
2026-05-17 10:24:19 +08:00
naibaandnaiba/CloudCode e38a0ef469 fix(cron): restrict task delivery by owner
Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
2026-05-17 10:24:19 +08:00
naibaandnaiba/CloudCode c4bea1ffd3 fix(notification): harden webhook request handling
Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
2026-05-17 10:24:19 +08:00
naiba 027cb5f4b7 chore: upgrade frontend 2026-04-05 10:00:11 +08:00
naibaandnaiba/CloudCode 241506e390 chore: bump Go to 1.26, update dependencies and frontend templates
Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
2026-03-20 11:23:03 +00:00
naiba 5c07c3befb Merge pull request #1177 from nezhahq/dependabot/go_modules/google.golang.org/grpc-1.79.3
chore(deps): bump google.golang.org/grpc from 1.76.0 to 1.79.3
2026-03-20 18:39:37 +08:00