GitHub Security Advisory: GHSA-hm2h-wwwh-g49x
Package: github.com/lin-snow/ech0 (GO)
Severity: MODERATE (CVSS 6.5)
CWE: CWE-863: Incorrect Authorization
Vulnerable versions: < 4.4.3
Patched in: 4.4.3

Summary: Ech0 Scope Bypass: profile:read Access Token Can Change Admin Password and Escalate to Unrestricted Session

Details:
## Summary

The `PUT /user` endpoint is protected by `RequireScopes("profile:read")`, which is a read-only scope. However, the endpoint performs write operations including password changes. An attacker who obtains an admin's restricted `profile:read` access token can change the admin's password, then login to receive an unrestricted session token that bypasses all scope enforcement.

## Details

The scope enforcement system defines granular scopes (e.g., `echo:read`, `echo:write`, `admin:user`) but has no `profile:write` scope. The `PUT /user` route is protected only by `profile:read`:

```go
// internal/router/user.go:40-44
appRouterGroup.AuthRouterGroup.PUT(
    "/user",
    middleware.RequireScopes(authModel.ScopeProfileRead),
    h.UserHandler.UpdateUser(),
)
```

The `RequireScopes` middleware bypasses all scope checks for session tokens, and for access tokens only verifies the token contains the listed scopes:

```go
// internal/middleware/scope.go:14-19
func RequireScopes(scopes ...string) gin.HandlerFunc {
    return func(ctx *gin.Context) {
        v := viewer.MustFromContext(ctx.Request.Context())
        if v.TokenType() == authModel.TokenTypeSession {
            ctx.Next()
            return
        }
        // ... checks access token has required scopes (line 53)
```

The `UpdateUser` service checks `user.IsAdmin` but does not verify the token's scope is sufficient for write operations:

```go
// internal/service/user/user.go:271-300
func (userService *UserService) UpdateUser(ctx context.Context, userdto model.UserInfoDto) error {
    userid := viewer.MustFromContext(ctx).UserID()
    user, err := userService.userRepository.GetUserByID(ctx, userid)
    // ...
    if !user.IsAdmin {
        return errors.New(commonModel.NO_PERMISSION_DENIED)
    }
    // ...
    if userdto.Password != "" && cryptoUtil.MD5Encrypt(userdto.Password) != user.Password {
        user.[CREDENTIAL_REDACTED].MD5Encrypt(userdto.Password)  // line 299
    }
```

After the password is changed, the attacker logs in via `POST /login` which calls `issueUserToken` → `CreateClaims`, producing a session token with `Type: "session"` (jwt.go:33). Session tokens bypass `RequireScopes` entirely, granting unrestricted API access.

**Escalation chain:** `profile:read` access token → password change → login → unrestricted session token (bypasses all scope checks) → full admin access including `admin:settings`, `admin:user`, `admin:token`, `file:write`, etc.

## PoC

```bash
# Prerequisites: Admin has created a profile:read access token for a read-only integration
# The attacker has obtained this token (e.g., from compromised integration, log leak, etc.)

ACCESS_TOKEN="<admin_profile_read_access_token>"
SERVER="http://localhost:8080"

# Step 1: Verify the token only has profile:read scope (can read profile)
curl -s -X GET "$SERVER/api/user" \
  -H "Authorization: Bearer $ACCESS_TOKEN"
# Expected: 200 OK with user profile data

# Step 2: Verify the token CANNOT access admin e