GHSA-VG99-7GJ7-2FR5

Vulnerability from github – Published: 2026-10-01 16:29 – Updated: 2026-10-01 16:29
VLAI
Summary
SiYuan: The reference filter for getRefIDs checks visibility but not the password tier, disclosing that password-protected documents reference a given block
Details

Summary

/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.

A function ten lines away in the same file does perform the full check, on the same input type.

Details

Route, identical at eef105683 (kernel/api/router.go:235) and dev a7ae96ce (:245):

ginServer.Handle("POST", "/api/block/getRefIDs", model.CheckAuth, getRefIDs)

No CheckReadonly, no CheckAdminRole.

The filter chain. getRefIDs (kernel/api/block.go:631) checks isEncryptedNotebookDeniedForPublish, calls model.GetBlockRefsInBox, then for read-only roles:

publishIgnore := model.GetInvisiblePublishAccess(publishAccess)
refDefs, originalRefBlockIDs = model.FilterRefDefsByPublishIgnore(publishIgnore, refDefs)

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:

for id, bt := range bts {
    if CheckPathAccessableByPublishIgnore(bt.BoxID, bt.Path, publishIgnore) {
        ret[id] = bt
    }
}

Inside that helper, CheckPublishAuthCookie, GetPathPasswordByPublishAccess, checkBlockTreeAccessableByPublishAccess and password all appear zero times.

The complete check exists in the same file. kernel/model/publish_access.go:431:

func checkBlockTreeAccessableByPublishAccess(c *gin.Context, publishAccess PublishAccess, bt *treenode.BlockTree) bool {
    if bt == nil || IsEncryptedBoxDeniedByPublishAccess(bt.BoxID) {
        return false
    }
    publishIgnore := filterDisablePublishAccess(publishAccess)
    passwordID, password := GetPathPasswordByPublishAccess(bt.BoxID, bt.Path, publishAccess)
    return CheckPathAccessableByPublishIgnore(bt.BoxID, bt.Path, publishIgnore) &&
           (password == "" || CheckPublishAuthCookie(c, passwordID, password))
}

Same package, same file, same *treenode.BlockTree input. One takes the context and evaluates the password; the other does not receive it and structurally cannot.

Route-level contrast. The adjacent getChildBlocks and getTailChildBlocks both carry model.CheckAdminRole.

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.

Proof of Concept

Kernel 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.

Tier of the referring document Anonymous result
Public reference returned (baseline)
Password-protected reference still returned
Hidden [], filtered
Forbidden [], filtered

The hidden and forbidden rows confirm the filter runs and works. The password-protected row is the defect.

Impact

An 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.

Scoped 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.

Confidentiality only.

Suggested fix

Thread *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.

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "Go",
        "name": "github.com/siyuan-note/siyuan/kernel"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "0.0.0-20260812083335-251596fc0de2"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-73606"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-863"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-10-01T16:29:35Z",
    "nvd_published_at": null,
    "severity": "MODERATE"
  },
  "details": "### 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\u0027s 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) \u0026\u0026\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\u0027s 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.",
  "id": "GHSA-vg99-7gj7-2fr5",
  "modified": "2026-10-01T16:29:35Z",
  "published": "2026-10-01T16:29:35Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/siyuan-note/siyuan/security/advisories/GHSA-vg99-7gj7-2fr5"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-73606"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/siyuan-note/siyuan"
    },
    {
      "type": "WEB",
      "url": "https://github.com/siyuan-note/siyuan/releases/tag/v3.8.0"
    },
    {
      "type": "WEB",
      "url": "https://www.vulncheck.com/advisories/siyuan-before-information-disclosure-via-getrefids"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:N/A:N",
      "type": "CVSS_V3"
    }
  ],
  "summary": "SiYuan: The reference filter for getRefIDs checks visibility but not the password tier, disclosing that password-protected documents reference a given block"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.

Loading…

Loading…

Loading…

Related by attack behaviour

Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.


Loading…