CVE-2026-53519: Nezha Monitoring Pre-Auth Path Traversal via /dashboard.. Prefix Confusion
A missing path-segment check in Nezha Monitoring's dashboard router lets any unauthenticated attacker read arbitrary server-side files, including the JWT signing key, by crafting a URL like /dashboard
The problem
The `fallbackToFrontend` catch-all handler in Nezha's Gin router uses `strings.HasPrefix(path, "/dashboard")` to decide whether a URL should be served as a frontend asset. This is a substring test, not a path-segment test, so `/dashboard../data/config.yaml` passes the check just as `/dashboard/login` does.
After stripping the prefix, `path.Join("admin-dist", "../data/config.yaml")` silently resolves to `data/config.yaml`. The handler calls `os.Stat` then `http.ServeFile` on that resolved path with no authentication check and no boundary validation. In a default deployment, `data/config.yaml` contains the HS256 `jwt_secret_key`, `agent_secret_key`, and OAuth2 secrets.
An attacker who reads that key can forge admin JWTs and take over the entire dashboard.
Proof of concept
# Step 1: leak the JWT secret (no auth required)
curl -s --path-as-is 'http://TARGET:8008/dashboard../data/config.yaml'
# Returns plaintext YAML including jwt_secret_key
# Encoded-dot variant (also works)
curl -s --path-as-is 'http://TARGET:8008/dashboard%2e%2e/data/config.yaml'
# Encoded-slash variant (also works)
curl -s --path-as-is 'http://TARGET:8008/dashboard..%2fdata/config.yaml'
# Step 2: leak the SQLite database (user IDs, bcrypt hashes, API tokens)
curl -s --path-as-is 'http://TARGET:8008/dashboard../data/sqlite.db' -o dump.db
# Step 3: forge an HS256 admin JWT with the leaked secret and attach as cookie
# nz-jwt cookie accepted by every admin handlerThree independent design choices combine to produce the vulnerability. First, `strings.HasPrefix` matches any URL whose first characters are `/dashboard`, so `dashboard..` is indistinguishable from `dashboard/`. Second, `path.Join` calls `path.Clean` internally and silently collapses the `..` component to escape the `admin-dist` root without returning an error.
Third, Go's `http.ServeFile` stdlib guard (`net/http.containsDotDot`) only fires when the *request URL itself* contains a standalone `..` segment; because the traversal token in `/dashboard../...` is the single path segment `dashboard..`, the guard never triggers, and the escape is only created after `TrimPrefix`, downstream of every existing defense.
The fix in v2.0.13 tightens the prefix test to require a trailing slash (`/dashboard/`), which makes `dashboard..` fail the check immediately. An additional `path.Clean` boundary check after strip ensures no joined path can escape the template root.
The fix
Upgrade to nezhahq/nezha v2.0.13 or later. The patch changes the prefix check from `strings.HasPrefix(path, "/dashboard")` to an exact match on `/dashboard/` (requiring the segment boundary), and adds a post-join path escape check before any filesystem call. As a defense-in-depth measure, consider setting the `NZ_JWTSECRETKEY` environment variable so the JWT secret is never written to `config.yaml` on disk.
Rotate your `jwt_secret_key` and `agent_secret_key` immediately if running a pre-2.0.13 instance that was internet-accessible.
Reported by sondt99.