Vulnerabilities in pkg:golang/github.com/lin-snow/ech0
PACKAGEGHSA-fpw6-hrg5-q5x5 is a HIGH (CVSS 7.4) logout panics, delete does not vulnerability (CWE-613: Insufficient Session Expiration) affecting Github.com/lin snow/ech0 libraries in the Go ecosystem. The issue involves logout panics, delete does not blacklist jti. Remediation is available by upgrading affected packages to version 1.4.8-0.20260503041146-eab62379c795.
pkg:golang/github.com/lin-snow/ech0
Vulnerability advisories affecting package pkg:golang/github.com/lin-snow/ech0. Select an advisory below to view technical details or fixes.
| Advisory ID | CVE / NVD | Severity | Summary |
|---|---|---|---|
| GHSA-fpw6-hrg5-q5x5 | - | HIGH | ech0's acess tokens with expiry=never cannot be revoked: logout panics, delete does not blacklist JTI |
| GHSA-mqxv-9rm6-w8qc | - | HIGH | Ech0: ParseAcceptLanguage `_` separator bypass enables ~70x CPU amplification via Accept-Language header in i18n.Middleware |
| GHSA-8mc6-xjpr-h98x | - | HIGH | Ech0 has Server-Side Request Forgery (SSRF) via Connect Handler fetchPeerConnectInfo |
| GHSA-rgj7-vg8v-j4wr | - | MODERATE | Ech0's Unauthenticated Like Endpoint Enables Arbitrary Engagement Metric Inflation |
| GHSA-69hx-63pv-f8f4 | - | MODERATE | Ech0 has Stored XSS via SVG Upload and Content-Type Validation Bypass in File Upload |
| GHSA-r2x7-427f-rq69 | - | MODERATE | Ech0 has SSRF via DNS Resolution Bypass in Webhook URL Validation |
| GHSA-w8jj-cwmc-wgq2 | - | MODERATE | Ech0's Missing Authorization on System Logs Allows Non-Admin Information Disclosure |
| GHSA-fwg7-53p4-g33c | - | MODERATE | Ech0 Comment Panel Endpoints Missing RequireScopes Middleware — Scoped Access Token Bypass |
| GHSA-hm2h-wwwh-g49x | - | MODERATE | Ech0 Scope Bypass: profile:read Access Token Can Change Admin Password and Escalate to Unrestricted Session |
| GHSA-cp79-9mwr-wr49 | - | MODERATE | Ech0: Missing authorization on dashboard log endpoints allows low-privilege users to access sensitive system logs |
| GHSA-4h9q-p5j4-xvvh | - | HIGH | Ech0: Scoped admin access tokens can bypass least-privilege controls on privileged endpoints, including backup export |
| GHSA-cqgf-f4x7-g6wc | CVE-2026-35037 | HIGH | Ech0: Unauthenticated SSRF in GetWebsiteTitle allows access to internal services and cloud metadata |
| GHSA-wc4h-2348-jc3p | CVE-2026-35036 | HIGH | Ech0 has Unauthenticated Server-Side Request Forgery in Website Preview Feature |
| GHSA-m983-7426-5hrj | CVE-2026-33638 | MODERATE | Ech0 authenticated user-list exposed data via public `/api/allusers` endpoint |
GHSA-fpw6-hrg5-q5x5 at a Glance
Security Impact
CVSS: 7.4
The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N).
How to Fix GHSA-fpw6-hrg5-q5x5
To remediate this vulnerability, upgrade all affected Github.com/lin snow/ech0 dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.
Upgrade github.com/lin-snow/ech0 to 1.4.8-0.20260503041146-eab62379c795
Run the following command in your project directory to verify that all resolved dependencies use the patched release:
inspect dependencies for github.com/lin-snow/ech0
How to Check if You Are Affected
To determine whether your application is affected by GHSA-fpw6-hrg5-q5x5, inspect your project dependencies for Github.com/lin snow/ech0:
inspect dependencies for github.com/lin-snow/ech0
Analyze the terminal output to locate any version of Github.com/lin snow/ech0 in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 1.4.8-0.20260503041146-eab62379c795), your application is affected.
Note: Verify declared build manifests and deployed runtime container environments.
Technical Details
Vulnerability Mechanism & Technical Analysis
This vulnerability is classified under CWE-613: Insufficient Session Expiration, where according to wasc, "insufficient session expiration is when a web site permits an attacker to reuse old session credentials or session ids for authorization.". In Github.com/lin snow/ech0, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.
Source Advisory Statement
GHSA-fpw6-hrg5-q5x5 Affected Github.com/lin snow/ech0 Versions
1 component(s)| Product / Package | Ecosystem | Affected Versions | Fixed Version |
|---|---|---|---|
|
github.com/lin-snow/ech0
|
Go |
< 1.4.8-0.20260503041146-eab62379c795
|
1.4.8-0.20260503041146-eab62379c795
|
References
Frequently Asked Questions
What is GHSA-fpw6-hrg5-q5x5?
Which Github.com/lin snow/ech0 versions are affected?
What version fixes GHSA-fpw6-hrg5-q5x5?
Is github.com/lin-snow/ech0 affected?
What is the CVSS score of GHSA-fpw6-hrg5-q5x5?
What CWE is associated with GHSA-fpw6-hrg5-q5x5?
How can I check Go dependencies for GHSA-fpw6-hrg5-q5x5?
Machine-readable OSV JSON
OSV v1.5 Schema
{
"schema_version" : "1.4.0",
"id" : "GHSA-fpw6-hrg5-q5x5",
"modified" : "2026-07-21T14:37:24Z",
"published" : "2026-05-07T21:34:01Z",
"aliases" : [ ],
"summary" : "ech0's acess tokens with expiry=never cannot be revoked: logout panics, delete does not blacklist JTI",
"details" : "## Summary\n\nAccess tokens created with the \"never expire\" option have no `exp` JWT claim. Three independent revocation mechanisms fail for this token type. Logout at `internal/handler/auth/auth.go:154` and `:163` dereferences `claims.ExpiresAt.Time`, panicking on the nil field so the token never hits the blacklist. `RevokeToken` at `internal/repository/auth/auth.go:45-50` skips when `remainTTL <= 0`. The admin's \"Delete token\" panel action at `internal/service/setting/access_token_service.go:183-185` removes the database record but does not call `RevokeToken` to blacklist the JTI. Once a never-expire token leaks, the JWT stays cryptographically valid until the admin rotates the signing key across the entire instance.\n\n## Details\n\nCreation path at `internal/util/jwt/jwt.go:103-105`:\n\n```go\n// expiry = 0 表示永不过期\nif expiry > 0 {\n claims.ExpiresAt = jwt.NewNumericDate(time.Now().UTC().Add(time.Duration(expiry) * time.Second))\n}\n```\n\nFor `NEVER_EXPIRY`, `expiry = 0` and the conditional skips. The resulting JWT has no `exp` claim. The middleware at `internal/middleware/auth.go` accepts it; the `jwt/v5` parser does not require `exp` by default.\n\nFailure mode 1, logout panic at `internal/handler/auth/auth.go:163`:\n\n```go\n// Refresh-token revocation at line 154 (safe in practice: refresh tokens always have exp).\n// Access-token revocation, same pattern, at line 163 (the bug):\nif claims, err := jwtUtil.ParseToken(authHeader[7:]); err == nil && claims.ID != \"\" {\n remaining := time.Until(claims.ExpiresAt.Time) // nil deref when ExpiresAt is nil\n h.authService.RevokeToken(claims.ID, remaining)\n}\n```\n\nFor a never-expire access token, `claims.ExpiresAt` is nil. `claims.ExpiresAt.Time` panics. Gin's Recovery middleware catches it and returns HTTP 500; the JTI never reaches `RevokeToken`. Line 154 shares the same pattern against refresh tokens, but refresh tokens are always issued with an expiry so the nil dereference does not fire there in practice.\n\nFailure mode 2, `RevokeToken` skip at `internal/repository/auth/auth.go:45-50`:\n\n```go\nfunc (authRepository *AuthRepository) RevokeToken(jti string, remainTTL time.Duration) {\n if jti == \"\" || remainTTL <= 0 {\n return\n }\n authRepository.cache.SetWithTTL(fmt.Sprintf(\"%s%s\", blacklistPrefix, jti), true, 1, remainTTL)\n}\n```\n\nEven if the logout path were patched to handle nil `ExpiresAt`, a caller computing `remainTTL = 0` would still skip the blacklist write.\n\nFailure mode 3, admin delete at `internal/service/setting/access_token_service.go:183-185`:\n\n```go\nreturn settingService.transactor.Run(ctx, func(txCtx context.Context) error {\n return settingService.settingRepository.DeleteAccessTokenByID(txCtx, id)\n})\n```\n\nDeletion removes the token's metadata row from the database. No call to `RevokeToken`, no write to the JTI blacklist. The JWT continues to validate because the signature is still authentic and the middleware does not consult the metadata table.\n\nThe only way to invalidate a compromised never-expire token is to rotate `JWT_SECRET`, which invalidates every token for every user across the whole instance.\n\n## Proof of Concept\n\nDefault install. Admin creates a never-expire access token; its revocation pathways all fail:\n\n```python\nimport requests, base64, json\nTARGET = \"http://localhost:8300\"\n\nowner = requests.post(f\"{TARGET}/api/login\",\n json={\"username\": \"owner\", \"password\": \"owner-pw\"}\n ).json()[\"data\"][\"access_token\"]\n\n# 1) Create a never-expire access token.\nr = requests.post(f\"{TARGET}/api/access-tokens\",\n headers={\"Authorization\": f\"Bearer {owner}\",\n \"content-type\": \"application/json\"},\n json={\"name\": \"poc-irrevocable\",\n \"expiry\": \"never\",\n \"scopes\": [\"profile:read\"],\n \"audience\": \"cli\"})\ntok = r.json()[\"data\"]\npad = lambda s: s + \"=\" * (-len(s) % 4)\npayload = json.loads(base64.urlsafe_b64decode(pad(tok.split(\".\")[1])))\nprint(f\" exp claim: {payload.get('exp')} (None = never expires)\")\nprint(f\" jti: {payload['jti']}\")\n\n# 2) Confirm it works.\nr = requests.get(f\"{TARGET}/api/user\", headers={\"Authorization\": f\"Bearer {tok}\"})\nprint(f\" token -> /api/user: HTTP {r.status_code}\")\n\n# 3) Failure mode #1 — logout panics on nil ExpiresAt.\nr = requests.post(f\"{TARGET}/api/auth/logout\",\n headers={\"Authorization\": f\"Bearer {tok}\"})\nprint(f\" logout: HTTP {r.status_code} (500 = Recovery middleware caught the panic)\")\n\n# 4) Failure mode #3 — admin delete does not blacklist the JTI.\nlisted = requests.get(f\"{TARGET}/api/access-tokens\",\n headers={\"Authorization\": f\"Bearer {owner}\"}).json()[\"data\"]\npoc_row = next(t for t in listed if t[\"name\"] == \"poc-irrevocable\")\nr = requests.delete(f\"{TARGET}/api/access-tokens/{poc_row['id']}\",\n headers={\"Authorization\": f\"Bearer {owner}\"})\nprint(f\" admin delete: HTTP {r.status_code} {r.text}\")\n\n# 5) Token should now be invalid if delete blacklisted. Test it.\nr = requests.get(f\"{TARGET}/api/user\", headers={\"Authorization\": f\"Bearer {tok}\"})\nprint(f\" after delete, token -> /api/user: HTTP {r.status_code}\")\nprint(f\" response body: {r.text[:150]}\")\n```\n\nObserved on v4.5.6 in the test container:\n\n```\nexp claim: None (None = never expires)\njti: 019daf86-6354-7c2d-9ff1-180de87667b3\ntoken -> /api/user: HTTP 200\nlogout: HTTP 500 (500 = Recovery middleware caught the panic)\nadmin delete: HTTP 200 {\"code\":1,\"msg\":\"删除访问令牌成功\",\"data\":null}\nafter delete, token -> /api/user: HTTP 200\nresponse body: {\"code\":1,\"msg\":\"获取用户信息成功\",\"data\":{\"id\":\"019daf76-b5d2-7778-a90a-e943872b2946\",\"username\":\"owner\",\"email\":\"owner@test.local\",\"is_admin\":true,\"is_owner\":true,...}}\n```\n\nAfter the admin \"deleted\" the token, the same JWT string still returns the owner's profile data. The token stays valid with no path to invalidate it short of rotating `JWT_SECRET`.\n\n## Impact\n\nThe \"never expire\" option is intended for CLI and integration use cases where rotating tokens is expensive. When one of those tokens leaks (configuration file committed to a public repo, developer laptop compromised, log file uploaded by mistake), the admin has no remediation that does not nuke every other user's session.\n\nA compromised token gives the attacker:\n\n- **Perpetual authenticated access** at whatever scopes the token holds until the JWT secret is rotated.\n- **Admin's \"revoke\" UI button lies.** The token row disappears from the panel but the bearer keeps working. The admin believes they mitigated the incident.\n- **Instance-wide blast radius on proper revocation.** The only working fix (rotate JWT_SECRET) forces every user to log in again and invalidates every other access token. Security incidents force operators into an all-or-nothing choice.\n\nPrecondition: token theft. A stolen token is the standard threat model for any long-lived credential; the point of revocation is that stolen credentials can be invalidated. Ech0 currently has no working path to do that for the \"never expire\" class.\n\n## Recommended Fix\n\nThree coordinated changes, matching the three failure modes:\n\n1. Replace \"never expire\" with a very long expiry (for example 10 years) so every token has a finite `exp` claim. This removes the conditional at `jwt.go:103` entirely:\n\n```go\nif expiry == model.NEVER_EXPIRY {\n expiry = int64((10 * 365 * 24 * time.Hour).Seconds())\n}\nclaims.ExpiresAt = jwt.NewNumericDate(time.Now().UTC().Add(time.Duration(expiry) * time.Second))\n```\n\n2. If the \"never expire\" semantics must be preserved, make logout handle nil `ExpiresAt` explicitly:\n\n```go\nif claims, err := jwtUtil.ParseToken(authHeader[7:]); err == nil && claims.ID != \"\" {\n var remaining time.Duration\n if claims.ExpiresAt != nil {\n remaining = time.Until(claims.ExpiresAt.Time)\n } else {\n remaining = 365 * 24 * time.Hour\n }\n h.authService.RevokeToken(claims.ID, remaining)\n}\n```\n\nAnd accept non-positive TTLs in `RevokeToken` by substituting a long default.\n\n3. Blacklist the JTI when admin deletes an access token:\n\n```go\ntok, err := settingService.settingRepository.GetAccessTokenByID(ctx, id)\nif err != nil {\n return err\n}\nif tok.JTI != \"\" {\n settingService.authRepo.RevokeToken(tok.JTI, 365*24*time.Hour)\n}\nreturn settingService.transactor.Run(ctx, func(txCtx context.Context) error {\n return settingService.settingRepository.DeleteAccessTokenByID(txCtx, id)\n})\n```\n\nAny two of the three changes close the gap; all three together make the revocation semantics match the admin's mental model.\n\n---\n*Found by [aisafe.io](https://aisafe.io)*",
"severity" : [ {
"type" : "CVSS_V3",
"score" : "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N"
} ],
"affected" : [ {
"package" : {
"ecosystem" : "Go",
"name" : "github.com/lin-snow/ech0"
},
"ranges" : [ {
"type" : "ECOSYSTEM",
"events" : [ {
"introduced" : "0"
}, {
"fixed" : "1.4.8-0.20260503041146-eab62379c795"
} ]
} ]
} ],
"references" : [ {
"type" : "WEB",
"url" : "https://github.com/lin-snow/Ech0/security/advisories/GHSA-fpw6-hrg5-q5x5"
}, {
"type" : "WEB",
"url" : "https://github.com/lin-snow/Ech0/commit/eab62379c795c3f4850a9ca938ae3f27d7171541"
}, {
"type" : "PACKAGE",
"url" : "https://github.com/lin-snow/Ech0"
} ],
"database_specific" : {
"cwe_ids" : [ "CWE-613", "CWE-755" ],
"severity" : "HIGH",
"github_reviewed" : true,
"github_reviewed_at" : "2026-05-07T21:34:01Z",
"nvd_published_at" : null
}
}