GitHub Security Advisory: GHSA-w8jj-cwmc-wgq2
Package: github.com/lin-snow/ech0 (GO)
Severity: MODERATE (CVSS 4.3)
CWE: CWE-862: Missing Authorization
Vulnerable versions: < 4.4.3
Patched in: 4.4.3

Summary: Ech0's Missing Authorization on System Logs Allows Non-Admin Information Disclosure

Details:
## Summary

The system log endpoints (`GET /api/system/logs`, `GET /api/system/logs/stream`, `WS /ws/system/logs`) lack authorization checks, allowing any authenticated non-admin user to read and stream all server logs. These logs contain error stack traces, internal file paths, module names, and arbitrary structured fields that facilitate reconnaissance for further attacks.

## Details

The dashboard routes in `internal/router/dashboard.go:7-8` register log endpoints on the `AuthRouterGroup` without any `RequireScopes` middleware:

```go
// internal/router/dashboard.go
func setupDashboardRoutes(appRouterGroup *AppRouterGroup, h *handler.Bundle) {
	appRouterGroup.AuthRouterGroup.GET("/system/logs", h.DashboardHandler.GetSystemLogs())
	appRouterGroup.AuthRouterGroup.GET("/system/logs/stream", h.DashboardHandler.SSESubscribeSystemLogs())
	appRouterGroup.WSRouterGroup.GET("/system/logs", h.DashboardHandler.WSSubscribeSystemLogs())
}
```

Compare with other admin-only routes that properly use `RequireScopes`:

```go
// internal/router/setting.go — every route has RequireScopes
appRouterGroup.AuthRouterGroup.GET("/settings",
    middleware.RequireScopes(authModel.ScopeAdminSettings),
    h.SettingHandler.GetSiteSettings())
```

The `AuthRouterGroup` only applies `JWTAuthMiddleware()` (router.go:36), which validates the JWT and sets the viewer context but does **not** check admin status. The `WSRouterGroup` (router.go:37) has no middleware at all — the WebSocket handler only calls `ParseToken` to verify the JWT signature (dashboard.go:74) without any role/scope validation.

The handler (`internal/handler/dashboard/dashboard.go:29-62`) and service (`internal/service/dashboard/dashboard.go:21-27`) contain zero authorization checks. Other services in the codebase properly enforce admin access:
- `internal/service/inbox/inbox.go:132` — `ensureAdmin()`
- `internal/service/migrator/migrator.go:360` — `ensureAdmin()`
- `internal/service/comment/comment.go:719` — `requireAdmin()`

Non-admin users are created with `IsAdmin: false` and `IsOwner: false` (`internal/service/user/user.go:220-221`) via the public registration endpoint.

The `LogEntry` struct (`internal/util/log/log.go:78-87`) exposes:
```go
type LogEntry struct {
    Time   string         `json:"time"`
    Level  string         `json:"level"`
    Msg    string         `json:"msg"`
    Module string         `json:"module,omitempty"`
    Caller string         `json:"caller,omitempty"`   // internal file paths
    Error  string         `json:"error,omitempty"`     // error stack traces
    Fields map[string]any `json:"fields,omitempty"`    // arbitrary structured data
    Raw    string         `json:"raw,omitempty"`       // raw log lines
}
```

## PoC

```bash
# 1. Register a non-admin user (system allows up to 5 users by default)
curl -X POST http://localhost:8080/api/register \
  -H 'Content-Type: application/json' \
  -d '{"username":"attacker","password":"Password123"}'

# 2. Login to get session t