GitHub Security Advisory: GHSA-m855-r557-5rc5
Package: github.com/amir20/dozzle (GO)
Severity: HIGH (CVSS 0.0)
CWE: CWE-284: Improper Access Control, CWE-639: Authorization Bypass Through User-Controlled Key
Vulnerable versions: < 1.29.1-0[PII_REDACTED]0338-620e59aa2463
Patched in: 1.29.1-0[PII_REDACTED]0338-620e59aa2463

Summary: Dozzle Agent Label-Based Access Control Bypass Allows Unauthorized Container Shell Access

Details:
### Summary
A flaw in Dozzle’s agent-backed shell endpoints allows a user restricted by label filters (for example, `label=env=dev`) to obtain an interactive root shell in out‑of‑scope containers (for example, `env=prod`) on the same agent host by directly targeting their container IDs.

**Note**: Tested on `v9.0.2` likely affects all versions with agent mode support.

### Details
When SIMPLE auth is enabled, Dozzle supports per‑user label filters in `users.yaml` (for example, filter: `label=env=dev`) to restrict which containers a user can see and interact with. These filters are propagated into the shell handlers, which resolve the target container via `h.hostService.FindContainer(hostKey(r), id, userLabels)` in both the attach and exec WebSocket endpoints, intending to limit shell access to containers within the user’s label scope.

For agent-backed hosts, the corresponding implementation ignores the label scope when resolving a container by ID ([agent_service.go#L27-L29](https://github.com/amir20/dozzle/blob/master/internal/support/container/agent_service.go#L27-L29)):
```go
func (a *agentService) FindContainer(ctx context.Context, id string, labels container.ContainerLabels) (container.Container, error) {
    return a.client.FindContainer(ctx, id)
}
```

As a result, an authenticated user configured with filter: `label=env=dev` and granted the shell role cannot see `env=prod` containers in the UI, but can still establish an interactive exec session into an `env=prod` container by calling `/api/hosts/{hostId}/containers/{containerId}/exec` (or `/attach`) with a valid JWT and the target container ID. This discrepancy between listing and exec/attach behavior breaks the intended label‑based isolation between environments or tenants for agent-backed deployments.

**Note**: The underlying Docker client implementation explicitly documents that `FindContainer` skips filters ([docker/client.go#L128-L137](https://github.com/amir20/dozzle/blob/master/internal/docker/client.go#L128-L137)):
```go
// Finds a container by id, skipping the filters
func (d *DockerClient) FindContainer(ctx context.Context, id string) (container.Container, error) {
    log.Debug().Str("id", id).Msg("Finding container")
    if json, err := d.cli.ContainerInspect(ctx, id); err == nil {
        return newContainerFromJSON(json, d.host.ID), nil
    } else {
        return container.Container{}, err
    }
}
```

**Note**: For reference, we can see the correct implementation in `ListContainers` ([agent_service.go#L43-L46](https://github.com/amir20/dozzle/blob/master/internal/support/container/agent_service.go#L43-L46)):
```go
func (a *agentService) ListContainers(ctx context.Context, labels container.ContainerLabels) ([]container.Container, error) {
	log.Debug().Interface("labels", labels).Msg("Listing containers from agent")
	return a.client.ListContainers(ctx, labels)
}
```
### PoC
```bash
# create new dir
$ cd /tmp && mkdir -p /tmp/dozzle && cd /tmp/dozzle && mkdir -p data

# run