# SiYuan: A parser differential between the WebSocket keepalive exemption and the session quarantine allows unauthenticated access to the broadcast event stream

SiYuan kernel WebSocket exempts keepalive sessions by substring match but quarantines by parsed query, so duplicated id/type params bypass auth and leak the live event stream.

- Payout: Research
- Severity: high
- Program: SiYuan
- Found by: Shirshakhtml
- Disclosed: 2026-10-23
- CVE: CVE-2026-73054
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-c8w8-3pqp-wr83
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-c8w8-3pqp-wr83
- Tags: area:web domain:web impact:data-leak kind:research platform:ghsa sev:high tech:go tech:websocket technique:parser-differential vuln:access-control vuln:auth-bypass

**Target:** SiYuan kernel WebSocket endpoint /ws
**Root cause:** The keepalive exemption uses strings.Contains on the raw RequestURI while the quarantine uses url.Values.Get, which returns only the first value of a repeated query key; a URI with duplicated id/type satisfies both checks differently.
**Steps:**
1. Configure kernel with --accessAuthCode set.
2. Baseline: connect to `/ws?app=siyuan&id=main&type=main` — rejected, closed 1002, 0 frames.
3. Control: connect to `/ws?app=siyuan&id=auth&type=auth` — connects, 0 frames (quarantined).
4. Attack: connect to `/ws?app=siyuan&id=main&type=main&id=auth&type=auth` with no credentials — connects and receives 19-30 broadcast frames.
**How found:** Noticed two checks on the same request URI used different parsing (raw substring vs parsed query), hypothesized a duplicated-key URI could satisfy both, and validated with baseline/control/attack socket comparison.
**Exploitation:** Unauthenticated remote caller opens a single WebSocket to the kernel port with duplicated id/type query parameters; substring exemption admits it, parsed query resolves id=main so it lands in the broadcast pool and receives live savedoc/transactions/filetreeSortChanged/create/backgroundtask frames.
**Escalation:** None beyond disclosure; no writes observed. Cross-origin reachability preserved due to missing CheckOrigin on /ws.
**Insight:** When an auth exemption and a containment check both read the same attacker-controlled URI, look for parse mismatches: substring vs parsed, first-value vs last-value on duplicated keys, or re-parsing after admission. Duplicate query keys are a cheap differential primitive.

## Proof of concept (copied exactly from the original report)

**Attack WebSocket URI with duplicated id/type params**

```
/ws?app=siyuan&id=main&type=main&id=auth&type=auth
```

**Substring-based keepalive exemption check**

```
authOk = strings.Contains(s.Request.RequestURI, "/ws?app=siyuan") &&
         strings.Contains(s.Request.RequestURI, "&id=auth&type=auth")
```

**Quarantine re-parses query with Get**

```
id = session.Request.URL.Query().Get("id")
return "auth" == id
```

