GitHub Security Advisory: GHSA-j643-x8pv-8m67
Package: github.com/amir20/dozzle (GO)
Severity: HIGH (CVSS 0.0)
CWE: CWE-346: Origin Validation Error
Vulnerable versions: <= 10.5.1
Patched in: unpatched

Summary: Dozzle's Cross-Site WebSocket Hijacking (CSWSH) on exec/attach endpointsbypasses authentication

Details:
## Summary

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`)**

```go
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`)**

```go
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.

1. 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`)
2. Victim visits the attacker's page while authenticated to Dozzle
3. Attacker's JavaScript opens `new WebSocket('wss://dozzle.example.com/api/hosts/{host}/containers/{id}/exec')`
4. Browser sends the JWT cookie (same-site, `SameSite=Lax` allows it)
5. Dozzle's `CheckOrigin` returns `true` — upgrade accepted
6. Auth middleware validates the JWT from the cookie — request authenticated
7. Attacker has a shell in the victim's authorized containers

## PoC (auth enabled)

**Setup — Dozzle with authentication + shell:**

docker-compose.yml:
```yaml
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:
```yaml
users:
  admin:
    name: Admin
    # [CREDENTIAL_REDACTED]
    [CREDENTIAL_REDACTED]