{"id":"CVE-2026-72812","aliases":["GHSA-wgwx-479j-23vq"],"title":"SiYuan: Missing authorization on refreshBacklink allows anonymous readers to trigger persistent server-side writes and unauthenticated resource amplification (publish mode)","summary":"SiYuan: Missing authorization on refreshBacklink allows anonymous readers to trigger persistent server-side writes and unauthenticated resource amplification (publish mode)","severity":"medium","cvss":6.5,"cwe":["CWE-862"],"vendor":"siyuan-note","product":"github.com/siyuan-note/siyuan/kernel","ecosystem":"go","affected":["github.com/siyuan-note/siyuan/kernel < 0.0.0-20260723002528-7d273c271ce1"],"patched":["github.com/siyuan-note/siyuan/kernel 0.0.0-20260723002528-7d273c271ce1"],"published":"2026-09-03","updated":"2026-09-03","source":"GHSA","sourceUrl":"https://github.com/advisories/GHSA-wgwx-479j-23vq","references":[{"url":"https://github.com/siyuan-note/siyuan/security/advisories/GHSA-wgwx-479j-23vq"},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-72812"},{"url":"https://github.com/siyuan-note/siyuan/commit/7d273c271ce193b9d3ee5751b596b8093ba84ada"},{"url":"https://www.vulncheck.com/advisories/siyuan-before-missing-authorization-via-refreshbacklink"},{"url":"https://github.com/advisories/GHSA-wgwx-479j-23vq"}],"tags":["ghsa","go"],"epss":0.00377,"epssPercentile":0.28983,"ingestedAt":"2026-09-03T23:10:02.687Z","slug":"CVE-2026-72812","body":"## Overview\n\n**CVE:** This vulnerability corresponds to [CVE-2026-72812](https://nvd.nist.gov/vuln/detail/CVE-2026-72812).\n\n### Summary\n\nThe `/api/ref/refreshBacklink` endpoint is gated by `CheckAuth` only. Unlike its mutating siblings, it carries no `CheckAdminRole`, no `CheckReadonly`, and no inline reader-role guard so it falls through all three authorization mechanisms the codebase uses to protect write operations. A publish `RoleReader` or the anonymous account when `Publish.Auth.Enable` is `false` can invoke it, forcing the server to flush its pending write-transaction queue, scan all references globally, load and parse referencing trees from disk, and enqueue database writes. This violates the read-only invariant (it writes even in a globally read-only workspace), provides an unauthenticated resource-amplification/DoS primitive, and applies no per-object access check to the caller-supplied ID.\n\n### Details\n\n**Route / auth tier.** `router.go`: `Handle(\"POST\", \"/api/ref/refreshBacklink\", model.CheckAuth, refreshBacklink)`, `CheckAuth` only. `CheckAuth` admits `RoleReader`; the publish proxy forwards port-6808 traffic with a Reader JWT (anonymous account when publish auth is disabled). Anonymous/reader reachable.\n\n**Guard fall-through.** SiYuan protects write handlers with one of three mechanisms: route-level `CheckAdminRole` (AV/riff/repo/sync/setting/snippet/notebook mutations), route-level `CheckReadonly` (filetree/block/attr/tag mutations), or an inline `IsReadOnlyRoleContext` check (e.g. `updateEmbedBlock`, `updateRecentDoc*Time`). `refreshBacklink` has none of the three.\n\n**Write path reached.** `refreshBacklink` calls `model.RefreshBacklink(id)`:\n- `FlushTxQueue()` — forces the pending write-transaction queue to disk.\n- `refreshRefsByDefID(defID)` → `QueryRefsByDefID(defID)` (global scan with encrypted-box fallback loop) → `filesys.LoadTrees(rootIDs)` (disk read and Lute parse of every referencing tree) → `sql.UpdateRefsTreeQueue(tree)` (enqueues DB writes) → ref-count task update.\n\nThe handler also does not consult `util.ReadOnly`, so it executes its writes even when the workspace is configured globally read-only.\n\n**No per-object authorization.** `defID` is attacker-controlled and receives no publish-access or ownership check, so a reader can force a reference reindex of any document, including publish-forbidden/unpublished ones (cross-scope).\n\n### Proof of Concept\n\nReproduced on a local instance (SiYuan running locally, publish mode enabled on port 6808, publish Basic Auth disabled), as an anonymous reader (no token):\n\n**Target endpoint executes the full write path:**\n```\nPOST http://127.0.0.1:6808/api/ref/refreshBacklink\n{\"id\":\"<any block id>\"}\n```\nReturns `{\"code\":0,\"msg\":\"\",\"data\":null}` HTTP 200 the handler ran to completion, flushing the transaction queue and enqueuing ref writes.\n\n**Controls: the guarded mutating siblings correctly reject the same anonymous session:**\n```\nPOST http://127.0.0.1:6808/api/tag/renameTag      → 403 (CheckAdminRole and CheckReadonly)\nPOST http://127.0.0.1:6808/api/block/foldBlock    → 403\nPOST http://127.0.0.1:6808/api/block/updateEmbedBlock → code 0 no-op (inline IsReadOnlyRoleContext blocks the write)\n```\nThe 200-vs-403 contrast confirms `refreshBacklink` is reachable and executes where its siblings are blocked.\n\n### Impact\n\nAn anonymous reader (publish mode with auth disabled) or any publish `RoleReader` can:\n\n- **Bypass the read-only invariant:** trigger persistent server-side writes (transaction-queue flush and reference reindex), including in a workspace configured globally read-only.\n- **Amplify resource use without authentication:** each call forces a transaction flush, a global reference scan, and disk read and parse of every referencing tree, with an attacker-controlled `id` and no rate limiting, a DoS primitive.\n- **Act cross-scope:** `defID` receives no publish-access check, so a reader can force reindexing of documents outside their publish scope. This is an integrity-invariant violation and a resource-amplification vector, not data corruption or injection, the caller cannot control the content of the writes, only trigger them. Impact is integrity-low and availability-low; no confidentiality impact and no attacker-controlled data reaches storage.\n\n### Suggested fix\n\nApply the same guard its mutating siblings use, add `CheckReadonly` (and `CheckAdminRole` if reference refresh is intended to be an authenticated operation) to the route, or an inline `IsReadOnlyRoleContext` check consistent with `updateEmbedBlock`. The endpoint should also honor `util.ReadOnly` and apply a publish-access check to `defID` so a reader cannot force cross-scope reindexing. More broadly, the three-way guard strategy (route middleware vs. inline check vs. none) is what allowed this handler to receive no gate at all; a structural backstop, a role-scoped route group for the mutation surface would prevent recurrence.\n\n## Affected packages\n\n- `github.com/siyuan-note/siyuan/kernel < 0.0.0-20260723002528-7d273c271ce1`\n\n## Remediation\n\nUpgrade to a patched release:\n\n- `github.com/siyuan-note/siyuan/kernel 0.0.0-20260723002528-7d273c271ce1`","depth":"sunlit","depthScore":36,"depthScoreParts":{"impact":35.8,"likelihood":0.1,"exploitation":0,"ransomware":0},"changes":[]}