GHSA-4PJG-X8QJ-33J5

Vulnerability from github – Published: 2026-10-23 18:31 – Updated: 2026-10-23 18:31
VLAI
Summary
SiYuan: authFilePublishAccess distinguishes hidden and forbidden documents from public and nonexistent ones, and issues a publish-auth cookie for documents it reports as forbidden
Details

Summary

/api/filetree/authFilePublishAccess is registered with CheckAuth only and is reachable anonymously. It never sets a failure code, so its outcome is signalled entirely by the response message and by whether a Set-Cookie header is present. Those two signals differ across access tiers, which lets an anonymous caller determine, for any candidate document identifier, whether that document is public or nonexistent, password-protected, or exists at the hidden or forbidden tier.

Separately, hidden and forbidden entries carry an empty password. Submitting password:"" therefore satisfies the equality check for them, and the server responds by issuing Set-Cookie: publish-auth-<docID> for a document whose own tier is forbidden.

Details

Route, identical on both refs:

ginServer.Handle("POST", "/api/filetree/authFilePublishAccess", model.CheckAuth, authFilePublishAccess)

No CheckAdminRole, no CheckReadonly.

Handler, kernel/api/filetree.go:1608 on the development branch and :1421 on master. ret.Code is never assigned on the failure path, so every response carries code:0. The branch is:

if item.Password == password {
    // SetPublishAuthCookie
} else {
    ret.Msg = Conf.Language(285)
}

The cookie is issued for forbidden documents. Entries at the hidden and forbidden tiers store Password == "". A request with password:"" therefore makes item.Password == password true for them, and the handler takes the success branch. The server issues a publish-auth cookie for a document it otherwise treats as inaccessible.

The cookie itself confers nothing, because those tiers are decided before any password check elsewhere in the codebase. The defect is that the endpoint's success signal does not correspond to access being granted, and that signal is observable.

Three distinguishable outcomes. With password:"", one request per candidate identifier:

Tier of the target Set-Cookie Body
Public (no publish-access entry) absent {"code":0,"msg":""}
Nonexistent identifier absent {"code":0,"msg":""}
Password-protected absent {"code":0,"msg":"Password is incorrect"}
Hidden present {"code":0,"msg":""}
Forbidden present {"code":0,"msg":""}

Public and nonexistent are indistinguishable from each other, which is correct. Everything else separates. A caller learns whether a given identifier names a password-protected document, or a document that exists at the hidden or forbidden tier, which is precisely the class the publish tiers exist to conceal.

Identifiers are readily available. Document and block identifiers appear in the DOM of published pages, in reference and backlink payloads, and in annotation reference data. The caller does not need to guess them.

A fourth response on the development branch. IsEncryptedPublishRuntimeTarget sets ret.Code = -1, which identifies the supplied identifier as belonging to an encrypted notebook. That is a membership oracle for encrypted notebooks, the same data class as GHSA-f2rw-w22v-54vh.

Proof of Concept

Publish mode enabled, Publish.Auth.Enable false, anonymous client. One request per candidate identifier, all with an empty password:

POST /api/filetree/authFilePublishAccess
{"id":"<candidate document id>","password":""}

Comparing the response message and the presence of Set-Cookie against the table above classifies the identifier. Documents at the hidden and forbidden tiers return a publish-auth cookie and an empty message, distinguishing them both from public documents and from identifiers that name nothing at all.

Impact

An anonymous reader in publish mode, or any publish RoleReader, can classify arbitrary document identifiers by access tier and confirm the existence of documents at the hidden and forbidden tiers. On the development branch the same endpoint additionally reveals whether an identifier belongs to an encrypted notebook.

Scoped precisely: no document content is disclosed and no privilege is gained, since the issued cookie is inert for these tiers. The value is as a confirmation primitive, turning an identifier obtained elsewhere into a definite statement about a document the reader is not permitted to see.

Confidentiality only, with no integrity or availability impact.

Suggested fix

Return an identical response for every failure and for every tier the caller may not access, with no Set-Cookie header and no distinguishing message. In particular, do not treat an empty stored password as a successful match: hidden and forbidden entries should short-circuit before the password comparison rather than satisfying it. The encrypted-notebook branch should return the same generic response as everything else rather than its own code.

A rate limit on this endpoint would be a reasonable additional hardening measure, though it does not address the disclosure above.

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "Go",
        "name": "github.com/siyuan-note/siyuan/kernel"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "0.0.0-20260731093152-c7522b8db586"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-73630"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-203"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-10-23T18:31:42Z",
    "nvd_published_at": null,
    "severity": "MODERATE"
  },
  "details": "### Summary\n\n`/api/filetree/authFilePublishAccess` is registered with `CheckAuth` only and is reachable anonymously. It never sets a failure code, so its outcome is signalled entirely by the response message and by whether a `Set-Cookie` header is present. Those two signals differ across access tiers, which lets an anonymous caller determine, for any candidate document identifier, whether that document is public or nonexistent, password-protected, or exists at the hidden or forbidden tier.\n\nSeparately, hidden and forbidden entries carry an empty password. Submitting `password:\"\"` therefore satisfies the equality check for them, and the server responds by issuing `Set-Cookie: publish-auth-\u003cdocID\u003e` for a document whose own tier is forbidden.\n\n### Details\n\n**Route,** identical on both refs:\n\n```go\nginServer.Handle(\"POST\", \"/api/filetree/authFilePublishAccess\", model.CheckAuth, authFilePublishAccess)\n```\n\nNo `CheckAdminRole`, no `CheckReadonly`.\n\n**Handler,** `kernel/api/filetree.go:1608` on the development branch and `:1421` on master. `ret.Code` is never assigned on the failure path, so every response carries `code:0`. The branch is:\n\n```go\nif item.Password == password {\n    // SetPublishAuthCookie\n} else {\n    ret.Msg = Conf.Language(285)\n}\n```\n\n**The cookie is issued for forbidden documents.** Entries at the hidden and forbidden tiers store `Password == \"\"`. A request with `password:\"\"` therefore makes `item.Password == password` true for them, and the handler takes the success branch. The server issues a publish-auth cookie for a document it otherwise treats as inaccessible.\n\nThe cookie itself confers nothing, because those tiers are decided before any password check elsewhere in the codebase. The defect is that the endpoint\u0027s success signal does not correspond to access being granted, and that signal is observable.\n\n**Three distinguishable outcomes.** With `password:\"\"`, one request per candidate identifier:\n\n| Tier of the target | `Set-Cookie` | Body |\n|---|---|---|\n| Public (no publish-access entry) | absent | `{\"code\":0,\"msg\":\"\"}` |\n| Nonexistent identifier | absent | `{\"code\":0,\"msg\":\"\"}` |\n| Password-protected | absent | `{\"code\":0,\"msg\":\"Password is incorrect\"}` |\n| Hidden | **present** | `{\"code\":0,\"msg\":\"\"}` |\n| Forbidden | **present** | `{\"code\":0,\"msg\":\"\"}` |\n\nPublic and nonexistent are indistinguishable from each other, which is correct. Everything else separates. A caller learns whether a given identifier names a password-protected document, or a document that exists at the hidden or forbidden tier, which is precisely the class the publish tiers exist to conceal.\n\n**Identifiers are readily available.** Document and block identifiers appear in the DOM of published pages, in reference and backlink payloads, and in annotation reference data. The caller does not need to guess them.\n\n**A fourth response on the development branch.** `IsEncryptedPublishRuntimeTarget` sets `ret.Code = -1`, which identifies the supplied identifier as belonging to an encrypted notebook. That is a membership oracle for encrypted notebooks, the same data class as `GHSA-f2rw-w22v-54vh`.\n\n### Proof of Concept\n\nPublish mode enabled, `Publish.Auth.Enable` false, anonymous client. One request per candidate identifier, all with an empty password:\n\n```\nPOST /api/filetree/authFilePublishAccess\n{\"id\":\"\u003ccandidate document id\u003e\",\"password\":\"\"}\n```\n\nComparing the response message and the presence of `Set-Cookie` against the table above classifies the identifier. Documents at the hidden and forbidden tiers return a publish-auth cookie and an empty message, distinguishing them both from public documents and from identifiers that name nothing at all.\n\n### Impact\n\nAn anonymous reader in publish mode, or any publish `RoleReader`, can classify arbitrary document identifiers by access tier and confirm the existence of documents at the hidden and forbidden tiers. On the development branch the same endpoint additionally reveals whether an identifier belongs to an encrypted notebook.\n\nScoped precisely: no document content is disclosed and no privilege is gained, since the issued cookie is inert for these tiers. The value is as a confirmation primitive, turning an identifier obtained elsewhere into a definite statement about a document the reader is not permitted to see.\n\nConfidentiality only, with no integrity or availability impact.\n\n### Suggested fix\n\nReturn an identical response for every failure and for every tier the caller may not access, with no `Set-Cookie` header and no distinguishing message. In particular, do not treat an empty stored password as a successful match: hidden and forbidden entries should short-circuit before the password comparison rather than satisfying it. The encrypted-notebook branch should return the same generic response as everything else rather than its own code.\n\nA rate limit on this endpoint would be a reasonable additional hardening measure, though it does not address the disclosure above.",
  "id": "GHSA-4pjg-x8qj-33j5",
  "modified": "2026-10-23T18:31:43Z",
  "published": "2026-10-23T18:31:42Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/siyuan-note/siyuan/security/advisories/GHSA-4pjg-x8qj-33j5"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-73630"
    },
    {
      "type": "WEB",
      "url": "https://github.com/siyuan-note/siyuan/commit/c7522b8db586ef1e1d6fa98bbe7ca0eb9d019efe"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/siyuan-note/siyuan"
    },
    {
      "type": "WEB",
      "url": "https://github.com/siyuan-note/siyuan/releases/tag/v3.7.4-alpha.4"
    },
    {
      "type": "WEB",
      "url": "https://www.vulncheck.com/advisories/siyuan-before-information-disclosure-via-authfilepublishaccess"
    }
  ],
  "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: authFilePublishAccess distinguishes hidden and forbidden documents from public and nonexistent ones, and issues a publish-auth cookie for documents it reports as forbidden"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Drag nodes to arrange them. Double-click a related vulnerability to open it, or a source to visit its link. The Physics button on the left tunes the layout and the simulation; the legend at the bottom left hides or shows nodes by kind and by sighting type.

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.
Vulnerability timeline

Sightings and recorded dates, grouped by day · UTC

JSON
Events included in the timeline

Loading timeline…


    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.