critical · 9.1CVE-2026-53519Jun 26, 2026

CVE-2026-53519: Nezha Monitoring Pre-Auth Path Traversal via /dashboard.. Prefix Confusion

Shubham Kandhare
Security Engagement Manager, SecureLayer7

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

Packagegithub.com/nezhahq/nezha
Ecosystemgo
Affected< 2.0.13
Fixed in2.0.13

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

bash
# 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 handler

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

References: [1][2][3]