Skip to content

Six publish-mode reader-facing endpoints filter results using the "invisible" list instead of the "disabled" (forbidden) list, leaking content from notebooks/documents an admin explicitly disabled from publishing (update: eight endpoints confirmed, see body)

High
88250 published GHSA-48p5-pffc-5r9p Aug 4, 2026

Package

gomod github.com/siyuan-note/siyuan/kernel (Go)

Affected versions

3.7.3

Patched versions

v3.7.4

Description

Found via the same technique as the MCP path-traversal finding: tracing
a class of very recently disclosed bugs (e.g. GHSA-p4qw-vx5p-g984,
"publish filter for database backlinks consults the forbidden list
instead of the visibility list", published Jul 30, 2026) to check every
other function with the same shape. Six more instances of the identical
root cause were found, none of them the same code path as the already-
published bug.

Summary

SiYuan's publish-access model has two independent, differently-scoped
controls per notebook/document, stored on the same PublishAccessItem
struct:

type PublishAccessItem struct {
    ID       string `json:"id"`
    Visible  bool   `json:"visible"`  // 是否发布可见 (shown in the published site's navigation)
    Password string `json:"password"`
    Disable  bool   `json:"disable"`  // 是否禁止发布 (forbidden from publish access entirely)
}

Visible=false only affects navigation/listing cosmetics, an
"unlisted" item is still meant to be reachable if a reader has a direct
reference to it (e.g. via search or a backlink), which is why the
canonical, correct access-control check,
checkBlockTreeAccessableByPublishAccess()
(kernel/model/publish_access.go:224), consults
GetDisablePublishAccess() (the Disable field), not
GetInvisiblePublishAccess() (the Visible field), as the actual
security boundary. Disable=true is meant to make a notebook/document
completely inaccessible to anonymous publish-mode readers regardless of
how they try to reach it.

Six separate reader-facing filter functions in the same file consult
GetInvisiblePublishAccess() instead of GetDisablePublishAccess(),
meaning content explicitly marked Disable=true by the workspace owner
is not filtered out of these six result sets, as long as it is also
Visible=true (a legitimate, independent combination an admin can set:
"listed in navigation" and "forbidden from publish access" are not
mutually exclusive settings). This is CWE-863 (Incorrect Authorization),
the same class and shape as GHSA-p4qw-vx5p-g984, but in six different
code paths that advisory did not cover.

Details

All six functions are in kernel/model/publish_access.go and share the
identical pattern, publishIgnore := GetInvisiblePublishAccess(publishAccess)
where the canonical check uses GetDisablePublishAccess:

Function Endpoint Handler location
FilterBlocksByPublishAccess POST /api/search/listInvalidBlockRefs, POST /api/attr/getBookmarkLabels, POST /api/search/fullTextSearchBlock kernel/api/search.go:59, kernel/api/bookmark.go:37, kernel/api/search.go:485/524/673
FilterSearchDocsByPublishAccess POST /api/filetree/searchDocs kernel/api/filetree.go:1102
FilterPathsByPublishAccess POST /api/ref/getBacklink, POST /api/ref/getBacklink2 kernel/api/ref.go:180-181, 229-230
FilterAssetContentByPublishAccess POST /api/search/getAssetContent, POST /api/search/getAssetContentByPath, POST /api/search/fullTextSearchAssetContent kernel/api/search.go:84, 110, 153
FilterCriteriaByPublishAccess POST /api/storage/getCriteria kernel/api/storage.go:252
FilterRecentDocsByPublishAccess POST /api/storage/getRecentDocs kernel/api/storage.go:336
FilterGraphByPublishIgnore (called with GetInvisiblePublishAccess inline, not even via a named wrapper) POST /api/graph/getGraph, POST /api/graph/getLocalGraph kernel/api/graph.go:93-97, 153-157
FilterTagsByPublishIgnore (same inline pattern) POST /api/tag/getTag kernel/api/tag.go:56-60

That makes eight confirmed reader-facing endpoints/functions sharing
this exact root cause, not six. The graph and tag endpoints don't even
go through a named Filter...ByPublishAccess wrapper function, the
call site itself does publishIgnore := model.GetInvisiblePublishAccess(publishAccess)
directly inline, e.g. kernel/api/graph.go:93-97:

if model.IsReadOnlyRoleContext(c) {
    publishAccess := model.GetPublishAccess()
    publishIgnore := model.GetInvisiblePublishAccess(publishAccess)
    nodes, links = model.FilterGraphByPublishIgnore(publishIgnore, nodes, links)
}

and identically in kernel/api/tag.go:56-60. This means the graph view
(document relationship visualization) and the tag list/counts both leak
the existence and metadata of Disable=true documents to anonymous
publish-mode visitors, in addition to the six functions above.

A ninth instance is in kernel/api/system.go:621-625, the handler for
POST /api/system/getConf (kernel/api/system.go:594, the endpoint the
frontend calls on load to fetch UI configuration, including the saved
UI layout/open-tabs state):

if model.IsReadOnlyRoleContext(c) {
    publishAccess := model.GetPublishAccess()
    publishIgnore := model.GetInvisiblePublishAccess(publishAccess)
    maskedConf = model.FilterConfByPublishIgnore(publishIgnore, maskedConf)
}

Same inline pattern, same wrong list. Impact here is narrower than the
content-disclosure cases (it affects FilterUILayoutByPublishIgnore,
which strips saved-layout tab entries pointing at inaccessible
documents), but it still means a Disable=true document's ID and the
fact that it was open in a saved layout tab is not filtered out purely
because it happens to be Visible=true, a metadata leak of the same
root cause.

Every one of these call sites is gated identically, confirmed by reading
each handler in full:

if model.IsReadOnlyRoleContext(c) {
    publishAccess := model.GetPublishAccess()
    <result> = model.Filter...ByPublishAccess(c, publishAccess, <result>)
}

IsReadOnlyRoleContext is true specifically for RoleReader/anonymous
publish-mode visitors, confirming this filtering exists precisely to
enforce the publish access boundary for untrusted readers, the same
boundary checkBlockTreeAccessableByPublishAccess enforces correctly
elsewhere.

For contrast, the function covering the already-published
GHSA-p4qw-vx5p-g984 (database/attribute-view backlinks),
FilterAttributeViewBacklinksByPublishAccess
(kernel/model/publish_access.go, further down the same file), was
fixed to use GetDisablePublishAccess correctly; this advisory's six
functions were not part of that fix and still use the other list.

Step-by-step reproduction

  1. As the workspace admin, enable Publish and set up publish access for
    at least two documents/notebooks:
    • Document A: Visible = true, Disable = true (explicitly
      forbidden from publishing, but not hidden from navigation)
    • Document B: Visible = true, Disable = false (a normal,
      legitimately published document)
  2. As an anonymous visitor (no session, no AccessAuthCode), issue a
    request that should never be able to see Document A's content, for
    example:
    curl -s -X POST http://<target>:6806/api/filetree/searchDocs \
      -H "Content-Type: application/json" \
      -d '{"k":"<a keyword unique to Document A content>"}'
  3. Expected if correctly protected (matching the behavior of the
    already-fixed database-backlinks case): Document A never appears,
    regardless of the search keyword, because Disable=true should make
    it fully inaccessible.
  4. Observed: Document A appears in the results, because
    FilterSearchDocsByPublishAccess only excludes items on the
    Invisible (Visible=false) list, and Document A is Visible=true.
  5. Repeat the same test against the other seven endpoints in the table
    above (getBacklink/getBacklink2 for a block inside Document A,
    getAssetContent/getAssetContentByPath/fullTextSearchAssetContent
    for an asset embedded in Document A, getCriteria for a saved search
    referencing Document A, getRecentDocs if Document A was recently
    opened, listInvalidBlockRefs/getBookmarkLabels/
    fullTextSearchBlock for blocks within Document A,
    POST /api/graph/getGraph/getLocalGraph to confirm Document A's
    node still appears in the anonymous visitor's graph view, and
    POST /api/tag/getTag to confirm tags used only in Document A still
    appear with correct counts), each leaks content from or about the
    Disable=true document in the same way.

Impact

A workspace owner who publishes a notebook and marks specific
documents within it as Disable=true, believing this makes them fully
inaccessible to anonymous visitors (the documented purpose of that
field, and the behavior the canonical access check correctly provides),
has that expectation silently violated across six different features:
search, document search, backlinks/backmentions, asset content search,
saved search criteria, and recent documents. Anonymous, unauthenticated
readers of the published site can discover and read content from
documents the owner explicitly tried to forbid from publishing, as long
as those documents remain Visible=true (not hidden from navigation),
which is an independent and entirely reasonable setting for an owner to
have chosen (e.g. "list it in the nav, but the content itself should
never be directly viewable without being explicitly enabled").


## Affected products

| Field | Value |
|---|---|
| Ecosystem | **Go** |
| Package name | `github.com/siyuan-note/siyuan/kernel` |
| Affected versions | Present at current HEAD (commit `eef1056`/`1673b75`, reviewed 2026-08-03); the six affected functions were not part of the `GHSA-p4qw-vx5p-g984` fix (Jul 30, 2026), which corrected the same root cause in a different function. |
| Patched versions | *(none yet, leave blank until a fix is released)* |

## Severity

| Field | Value |
|---|---|
| Vector string | `CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N` |
| Score | **7.5 (High)**, matching the severity class of the already-published sibling advisory: network vector, low complexity, no privileges or user interaction required (fully anonymous), high confidentiality impact via disclosure of content the owner explicitly marked forbidden, no integrity/availability impact since these are all read-only endpoints. |

## Weaknesses (CWE)

- **CWE-863**: Incorrect Authorization (primary), same classification as `GHSA-p4qw-vx5p-g984`

## Notes for filing
- This is the same root cause and shape as `GHSA-p4qw-vx5p-g984`, just
  in six different functions that fix did not reach. Worth linking to
  it directly when filing, and worth the maintainers doing one final
  pass grepping `GetInvisiblePublishAccess` across
  `kernel/model/publish_access.go` to confirm no seventh instance
  remains, since this is now the second time this exact confusion (
  `Visible`/navigation list vs. `Disable`/access-control list) has
  appeared across multiple call sites in the same file.
- Suggested fix: change all six functions to call
  `GetDisablePublishAccess(publishAccess)` instead of
  `GetInvisiblePublishAccess(publishAccess)`, matching the canonical
  `checkBlockTreeAccessableByPublishAccess` pattern. Given how easy this
  specific mix-up has been to reintroduce, consider consolidating all
  publish-access filtering through one shared helper that takes the
  correct list internally, rather than requiring every new filter
  function to remember to call the right getter.

Severity

High

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Network
Attack complexity
Low
Privileges required
None
User interaction
None
Scope
Unchanged
Confidentiality
High
Integrity
None
Availability
None

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N

CVE ID

No known CVE

Weaknesses

Use of Password Hash Instead of Password for Authentication

The product records password hashes in a data store, receives a hash of a password from a client, and compares the supplied hash to the hash obtained from the data store. Learn more on MITRE.

Credits