{"id":"CVE-2026-73606","aliases":["GHSA-vg99-7gj7-2fr5"],"title":"SiYuan: The reference filter for getRefIDs checks visibility but not the password tier, disclosing that password-protected documents reference a given block","summary":"SiYuan: The reference filter for getRefIDs checks visibility but not the password tier, disclosing that password-protected documents reference a given block","severity":"medium","cvss":5.8,"cwe":["CWE-863"],"vendor":"siyuan-note","product":"github.com/siyuan-note/siyuan/kernel","ecosystem":"go","affected":["github.com/siyuan-note/siyuan/kernel < 0.0.0-20260812083335-251596fc0de2"],"patched":["github.com/siyuan-note/siyuan/kernel 0.0.0-20260812083335-251596fc0de2"],"published":"2026-10-01","updated":"2026-10-01","sourceUpdated":"2026-10-01T16:29:36Z","source":"GHSA","sourceUrl":"https://github.com/advisories/GHSA-vg99-7gj7-2fr5","references":[{"url":"https://github.com/siyuan-note/siyuan/security/advisories/GHSA-vg99-7gj7-2fr5"},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-73606"},{"url":"https://github.com/siyuan-note/siyuan/releases/tag/v3.8.0"},{"url":"https://www.vulncheck.com/advisories/siyuan-before-information-disclosure-via-getrefids"},{"url":"https://github.com/advisories/GHSA-vg99-7gj7-2fr5"}],"tags":["ghsa","go"],"epss":0.00325,"epssPercentile":0.23198,"ingestedAt":"2026-10-01T18:55:42.381Z","slug":"CVE-2026-73606","body":"## Overview\n\n### Summary\n\n`/api/block/getRefIDs` filters its results for reader roles through a helper that checks only the visibility tiers. The password tier is not checked, because the helper does not receive the request context and therefore cannot evaluate the publish auth cookie. A reader who has not entered a document's publish password learns that the document references a given block.\n\nA function ten lines away in the same file does perform the full check, on the same input type.\n\n### Details\n\n**Route,** identical at `eef105683` (`kernel/api/router.go:235`) and dev `a7ae96ce` (`:245`):\n\n```go\nginServer.Handle(\"POST\", \"/api/block/getRefIDs\", model.CheckAuth, getRefIDs)\n```\n\nNo `CheckReadonly`, no `CheckAdminRole`.\n\n**The filter chain.** `getRefIDs` (`kernel/api/block.go:631`) checks `isEncryptedNotebookDeniedForPublish`, calls `model.GetBlockRefsInBox`, then for read-only roles:\n\n```go\npublishIgnore := model.GetInvisiblePublishAccess(publishAccess)\nrefDefs, originalRefBlockIDs = model.FilterRefDefsByPublishIgnore(publishIgnore, refDefs)\n```\n\n`FilterRefDefsByPublishIgnore` (`kernel/model/publish_access.go:1324`) collects the reference and definition identifiers, resolves their block trees, and delegates the decision to `FilterBlockTreesByPublishIgnore` (`:1314`), whose entire body is:\n\n```go\nfor id, bt := range bts {\n    if CheckPathAccessableByPublishIgnore(bt.BoxID, bt.Path, publishIgnore) {\n        ret[id] = bt\n    }\n}\n```\n\nInside that helper, `CheckPublishAuthCookie`, `GetPathPasswordByPublishAccess`, `checkBlockTreeAccessableByPublishAccess` and `password` all appear zero times.\n\n**The complete check exists in the same file.** `kernel/model/publish_access.go:431`:\n\n```go\nfunc checkBlockTreeAccessableByPublishAccess(c *gin.Context, publishAccess PublishAccess, bt *treenode.BlockTree) bool {\n    if bt == nil || IsEncryptedBoxDeniedByPublishAccess(bt.BoxID) {\n        return false\n    }\n    publishIgnore := filterDisablePublishAccess(publishAccess)\n    passwordID, password := GetPathPasswordByPublishAccess(bt.BoxID, bt.Path, publishAccess)\n    return CheckPathAccessableByPublishIgnore(bt.BoxID, bt.Path, publishIgnore) &&\n           (password == \"\" || CheckPublishAuthCookie(c, passwordID, password))\n}\n```\n\nSame package, same file, same `*treenode.BlockTree` input. One takes the context and evaluates the password; the other does not receive it and structurally cannot.\n\n**Route-level contrast.** The adjacent `getChildBlocks` and `getTailChildBlocks` both carry `model.CheckAdminRole`.\n\n**Note on a prior assessment.** `getRefIDs` has been described as correctly filtered because it calls a publish-access filter. It does. The filter it calls covers the visibility tiers only.\n\n### Proof of Concept\n\nKernel 3.7.2, publish mode on port 6808, `Publish.Auth.Enable` false, anonymous client. A public document referenced from a second document whose publish tier was changed between runs. `getRefIDs` was called anonymously each time.\n\n| Tier of the referring document | Anonymous result |\n|---|---|\n| Public | reference returned (baseline) |\n| Password-protected | **reference still returned** |\n| Hidden | `[]`, filtered |\n| Forbidden | `[]`, filtered |\n\nThe hidden and forbidden rows confirm the filter runs and works. The password-protected row is the defect.\n\n### Impact\n\nAn anonymous reader in publish mode, or any publish `RoleReader`, learns that a password-protected document contains a reference to a given block, without entering that document's password, and receives the block identifiers involved.\n\nScoped precisely: the response carries identifiers only. `type RefDefs { RefID string; DefIDs []string }`, returned alongside `originalRefBlockIDs`, a map of identifier to identifier. There is no reference text, title or content. The disclosure is the existence of a relationship, plus identifiers usable as input to other endpoints.\n\nConfidentiality only.\n\n### Suggested fix\n\nThread `*gin.Context` into `FilterRefDefsByPublishIgnore` and `FilterBlockTreesByPublishIgnore`, and use `checkBlockTreeAccessableByPublishAccess` in place of the bare `CheckPathAccessableByPublishIgnore` call, so the password tier and the encrypted-box check are both applied.\n\n## Affected packages\n\n- `github.com/siyuan-note/siyuan/kernel < 0.0.0-20260812083335-251596fc0de2`\n\n## Remediation\n\nUpgrade to a patched release:\n\n- `github.com/siyuan-note/siyuan/kernel 0.0.0-20260812083335-251596fc0de2`","depth":"sunlit","depthScore":32,"depthScoreParts":{"impact":31.9,"likelihood":0.1,"exploitation":0,"ransomware":0},"changes":[]}