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
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.
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.
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.
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.
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.
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.
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).
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.
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.
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.
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.
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>
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>
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>
- 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>
- 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>
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>