Every CVE Search/ Get API key

CVE-2026-79664

Ech0 before 4.7.3 Access Token Revocation Bypass

9.1 CriticalFix available

Ech0 before 4.7.3 fails to properly revoke access tokens created with never-expire option, allowing attackers to maintain perpetual authenticated access after token theft. Three independent revocation mechanisms fail: logout panics on nil ExpiresAt field, RevokeToken skips when remainTTL is zero, and admin delete does not blacklist the JTI, leaving stolen tokens cryptographically valid until JWT secret rotation.

Attack profile

How easy it is to exploit

Attack vector
Network
Complexity
High
Privileges needed
None
User interaction
None

What an attacker can affect

Confidentiality
High
Integrity
High
Availability
None

Exploit likelihood

0.36%Up 0.1 points since Aug 26, 2026
90%50%10%1%

What each source reports

CVE List Score used 9.1

Ech0 before 4.7.3 Access Token Revocation Bypass

Ech0 before 4.7.3 fails to properly revoke access tokens created with never-expire option, allowing attackers to maintain perpetual authenticated access after token theft. Three independent revocation mechanisms fail: logout panics on nil ExpiresAt field, RevokeToken skips when remainTTL is zero, and admin delete does not blacklist the JTI, leaving stolen tokens cryptographically valid until JWT secret rotation.

Record
CVE-2026-79664
CVSS 4.0
9.1 CriticalCVSS:4.0/AV:N/AC:H/AT:P/PR:N/UI:N/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N
CVSS 3.1
7.4 HighCVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N
Weakness
CWE-613
Updated
2026-11-02
EUVDEUVD-2026-65450 9.1

Ech0 before 4.7.3 fails to properly revoke access tokens created with never-expire option, allowing attackers to maintain perpetual authenticated access after token theft. Three independent revocation mechanisms fail: logout panics on nil ExpiresAt field, RevokeToken skips when remainTTL is zero, and admin delete does not blacklist the JTI, leaving stolen tokens cryptographically valid until JWT secret rotation.

Record
EUVD-2026-65450
CVSS 4.0
9.1 CriticalCVSS:4.0/AV:N/AC:H/AT:P/PR:N/UI:N/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N
Updated
2026-11-02
GitHub AdvisoriesGHSA-3r5q-vfpj-wprr 9.1

Ech0 before 4.7.3 fails to properly revoke access tokens created with never-expire option,...

Ech0 before 4.7.3 fails to properly revoke access tokens created with never-expire option, allowing attackers to maintain perpetual authenticated access after token theft. Three independent revocation mechanisms fail: logout panics on nil ExpiresAt field, RevokeToken skips when remainTTL is zero, and admin delete does not blacklist the JTI, leaving stolen tokens cryptographically valid until JWT secret rotation.

Record
GHSA-3r5q-vfpj-wprr
CVSS 4.0
9.1 CriticalCVSS:4.0/AV:N/AC:H/AT:P/PR:N/UI:N/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X
CVSS 3.1
7.4 HighCVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N
Weakness
CWE-613
Updated
2026-11-02
GitHub AdvisoriesGHSA-fpw6-hrg5-q5x5 7.4

ech0's acess tokens with expiry=never cannot be revoked: logout panics, delete does not blacklist JTI

Summary

Access 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.

Details

Creation path at internal/util/jwt/jwt.go:103-105:

// expiry = 0 表示永不过期
if expiry > 0 {
    claims.ExpiresAt = jwt.NewNumericDate(time.Now().UTC().Add(time.Duration(expiry) * time.Second))
}

For 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.

Failure mode 1, logout panic at internal/handler/auth/auth.go:163:

// Refresh-token revocation at line 154 (safe in practice: refresh tokens always have exp).
// Access-token revocation, same pattern, at line 163 (the bug):
if claims, err := jwtUtil.ParseToken(authHeader[7:]); err == nil && claims.ID != "" {
    remaining := time.Until(claims.ExpiresAt.Time)  // nil deref when ExpiresAt is nil
    h.authService.RevokeToken(claims.ID, remaining)
}

For 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.

Failure mode 2, RevokeToken skip at internal/repository/auth/auth.go:45-50:

func (authRepository *AuthRepository) RevokeToken(jti string, remainTTL time.Duration) {
    if jti == "" || remainTTL <= 0 {
        return
    }
    authRepository.cache.SetWithTTL(fmt.Sprintf("%s%s", blacklistPrefix, jti), true, 1, remainTTL)
}

Even if the logout path were patched to handle nil ExpiresAt, a caller computing remainTTL = 0 would still skip the blacklist write.

Failure mode 3, admin delete at internal/service/setting/access_token_service.go:183-185:

return settingService.transactor.Run(ctx, func(txCtx context.Context) error {
    return settingService.settingRepository.DeleteAccessTokenByID(txCtx, id)
})

Deletion 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.

The 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.

Proof of Concept

Default install. Admin creates a never-expire access token; its revocation pathways all fail:

import requests, base64, json
TARGET = "http://localhost:8300"

owner = requests.post(f"{TARGET}/api/login",
                      json={"username": "owner", "password": "owner-pw"}
                     ).json()["data"]["access_token"]

# 1) Create a never-expire access token.
r = requests.post(f"{TARGET}/api/access-tokens",
                  headers={"Authorization": f"Bearer {owner}",
                           "content-type": "application/json"},
                  json={"name": "poc-irrevocable",
                        "expiry": "never",
                        "scopes": ["profile:read"],
                        "audience": "cli"})
tok = r.json()["data"]
pad = lambda s: s + "=" * (-len(s) % 4)
payload =…
Record
GHSA-fpw6-hrg5-q5x5
CVSS 3.1
7.4 HighCVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N
Weakness
CWE-613, CWE-755
Updated
2026-11-02
NVD 9.1

Ech0 before 4.7.3 fails to properly revoke access tokens created with never-expire option, allowing attackers to maintain perpetual authenticated access after token theft. Three independent revocation mechanisms fail: logout panics on nil ExpiresAt field, RevokeToken skips when remainTTL is zero, and admin delete does not blacklist the JTI, leaving stolen tokens cryptographically valid until JWT secret rotation.

Record
CVE-2026-79664
CVSS 4.0
9.1 CriticalCVSS:4.0/AV:N/AC:H/AT:P/PR:N/UI:N/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X
CVSS 3.1
7.4 HighCVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N
Weakness
CWE-613
Updated
2026-11-02
OSV 9.1

Ech0 before 4.7.3 Access Token Revocation Bypass

Ech0 before 4.7.3 fails to properly revoke access tokens created with never-expire option, allowing attackers to maintain perpetual authenticated access after token theft. Three independent revocation mechanisms fail: logout panics on nil ExpiresAt field, RevokeToken skips when remainTTL is zero, and admin delete does not blacklist the JTI, leaving stolen tokens cryptographically valid until JWT secret rotation.

Record
CVE-2026-79664
CVSS 4.0
9.1 CriticalCVSS:4.0/AV:N/AC:H/AT:P/PR:N/UI:N/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N
Weakness
CWE-613
Updated
2026-11-02
OSVGHSA-fpw6-hrg5-q5x5 7.4

ech0's acess tokens with expiry=never cannot be revoked: logout panics, delete does not blacklist JTI

Summary

Access 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.

Details

Creation path at internal/util/jwt/jwt.go:103-105:

// expiry = 0 表示永不过期
if expiry > 0 {
    claims.ExpiresAt = jwt.NewNumericDate(time.Now().UTC().Add(time.Duration(expiry) * time.Second))
}

For 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.

Failure mode 1, logout panic at internal/handler/auth/auth.go:163:

// Refresh-token revocation at line 154 (safe in practice: refresh tokens always have exp).
// Access-token revocation, same pattern, at line 163 (the bug):
if claims, err := jwtUtil.ParseToken(authHeader[7:]); err == nil && claims.ID != "" {
    remaining := time.Until(claims.ExpiresAt.Time)  // nil deref when ExpiresAt is nil
    h.authService.RevokeToken(claims.ID, remaining)
}

For 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.

Failure mode 2, RevokeToken skip at internal/repository/auth/auth.go:45-50:

func (authRepository *AuthRepository) RevokeToken(jti string, remainTTL time.Duration) {
    if jti == "" || remainTTL <= 0 {
        return
    }
    authRepository.cache.SetWithTTL(fmt.Sprintf("%s%s", blacklistPrefix, jti), true, 1, remainTTL)
}

Even if the logout path were patched to handle nil ExpiresAt, a caller computing remainTTL = 0 would still skip the blacklist write.

Failure mode 3, admin delete at internal/service/setting/access_token_service.go:183-185:

return settingService.transactor.Run(ctx, func(txCtx context.Context) error {
    return settingService.settingRepository.DeleteAccessTokenByID(txCtx, id)
})

Deletion 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.

The 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.

Proof of Concept

Default install. Admin creates a never-expire access token; its revocation pathways all fail:

import requests, base64, json
TARGET = "http://localhost:8300"

owner = requests.post(f"{TARGET}/api/login",
                      json={"username": "owner", "password": "owner-pw"}
                     ).json()["data"]["access_token"]

# 1) Create a never-expire access token.
r = requests.post(f"{TARGET}/api/access-tokens",
                  headers={"Authorization": f"Bearer {owner}",
                           "content-type": "application/json"},
                  json={"name": "poc-irrevocable",
                        "expiry": "never",
                        "scopes": ["profile:read"],
                        "audience": "cli"})
tok = r.json()["data"]
pad = lambda s: s + "=" * (-len(s) % 4)
payload =…
Record
GHSA-fpw6-hrg5-q5x5
CVSS 3.1
7.4 HighCVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N
Weakness
CWE-613, CWE-755
Updated
2026-11-02
OSVGO-2026-5375 No score

ech0's acess tokens with expiry=never cannot be revoked: logout panics, delete does not blacklist JTI in github.com/lin-snow/ech0

ech0's acess tokens with expiry=never cannot be revoked: logout panics, delete does not blacklist JTI in github.com/lin-snow/ech0

Record
GO-2026-5375
Updated
2026-11-02

Affected products

Affected packages

Affected packages
PackageAffectedFixed
github.com/lin-snow/Ech0
Go
before 1.4.8-0.20260503041146-eab62379c795
1.4.8-0.20260503041146-eab62379c795
github.com/lin-snow/ech0
Go
before 1.4.8-0.20260503041146-eab62379c795
1.4.8-0.20260503041146-eab62379c795

History

History
RecordedChangedSources
first indexedCVE List, EUVD, GitHub Advisories, NVD, OSV

Dates are when Every CVE recorded each change. Upstream dates may differ.

In short

CVE-2026-79664 is a critical vulnerability (CVSS 9.1) in Lin-snow Ech0, published May 7, 2026. No source lists public exploit code. EPSS puts the chance of exploitation in the next 30 days at 0.36%, the 29th percentile. Fixed in github.com/lin-snow/Ech0 1.4.8-0.20260503041146-eab62379c795.

References

This page doesn't list everything

Left out here: the rest of 2 source descriptions. The JSON record has them.

MoveEnter Open