GitHub Security Advisory: GHSA-5f5r-95pg-xrpm
Package: github.com/henrygd/beszel (GO)
Severity: LOW (CVSS 3.5)
CWE: CWE-184: Incomplete List of Disallowed Inputs
Vulnerable versions: <= 0.18.6
Patched in: 0.18.7

Summary: Beszel has an IDOR in hub API endpoints that read system ID from URL parameter

Details:
## Summary
Some API endpoints in the Beszel hub accept a user-supplied system ID and proceed without further checks that the user should have access to that system. As a result, any authenticated user can access these routes for any system if they know the system's ID.

System IDs are random 15 character alphanumeric strings, and are not exposed to all users. However, it is theoretically possible for an authenticated user to enumerate a valid system ID via web API. To use the `containers` endpoints, the user would also need to enumerate a container ID, which is 12 digit hexadecimal string.

## Affected Component

- **File:** `internal/hub/api.go`, lines 283–361
- **Endpoints:**
  - `GET /api/beszel/containers/logs?system=SYSTEM_ID&container=CONTAINER_ID`
  - `GET /api/beszel/containers/info?system=SYSTEM_ID&container=CONTAINER_ID`
  - `GET /api/beszel/systemd/info?system=SYSTEM_ID&service=SERVICE_NAME`
  - `POST /api/beszel/smart/refresh?system=SYSTEM_ID`
- **Commit:** c7261b56f1bfb9ae57ef0856a0052cabb2fd3b84

## Vulnerable Code

The `containerRequestHandler` function retrieves a system by ID but never verifies the authenticated user is a member of that system:

```go
// internal/hub/api.go:283-305
func (h *Hub) containerRequestHandler(e *core.RequestEvent, fetchFunc func(*systems.System, string) (string, error), responseKey string) error {
    systemID := e.Request.URL.Query().Get("system")
    containerID := e.Request.URL.Query().Get("container")

    if systemID == "" || containerID == "" {
        return e.JSON(http.StatusBadRequest, map[string]string{"error": "system and container parameters are required"})
    }
    if !containerIDPattern.MatchString(containerID) {
        return e.JSON(http.StatusBadRequest, map[string]string{"error": "invalid container parameter"})
    }

    system, err := h.sm.GetSystem(systemID)
    // ^^^ No authorization check: e.Auth.Id is never verified against system.users
    if err != nil {
        return e.JSON(http.StatusNotFound, map[string]string{"error": "system not found"})
    }

    data, err := fetchFunc(system, containerID)
    if err != nil {
        return e.JSON(http.StatusNotFound, map[string]string{"error": err.Error()})
    }

    return e.JSON(http.StatusOK, map[string]string{responseKey: data})
}
```

The same pattern applies to `getSystemdInfo` (lines 322–340) and `refreshSmartData` (lines 342–361).

Meanwhile, the standard PocketBase collection API enforces proper membership checks:

```go
// internal/hub/collections.go:56-57
systemsMemberRule := authenticatedRule + " && users.id ?= @request.auth.id"
systemMemberRule  := authenticatedRule + " && system.users.id ?= @request.auth.id"
```

These rules are only applied to the PocketBase collection endpoints, **not** to the custom routes registered on `apiAuth`.

### PoC
**The proof:** The standard PocketBase API returns `404` (system not found) for unassigned systems. The custom endpoints resolve the system, contact the agent, and return data — p